From db0a12387d4c2c26089f1b19f1a95f71e595e896 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Sun, 21 Jun 2026 11:04:08 +0200 Subject: [PATCH 01/33] feat(framework): Add FastAPI scaffold --- framework/FASTAPI.md | 29 ++ .../py/flwr/supercore/routers/__init__.py | 15 + .../flwr/supercore/routers/health/__init__.py | 20 + .../flwr/supercore/routers/health/router.py | 32 ++ framework/py/flwr/superlink/main.py | 35 ++ .../py/flwr/superlink/routers/__init__.py | 15 + .../superlink/routers/control/__init__.py | 20 + .../flwr/superlink/routers/control/router.py | 32 ++ .../superlink/routers/runtime/__init__.py | 20 + .../flwr/superlink/routers/runtime/router.py | 32 ++ framework/py/flwr/supernode/main.py | 34 ++ .../py/flwr/supernode/routers/__init__.py | 15 + .../supernode/routers/runtime/__init__.py | 20 + .../flwr/supernode/routers/runtime/router.py | 32 ++ framework/pyproject.toml | 6 +- framework/uv.lock | 462 ++++++++++++++++-- 16 files changed, 778 insertions(+), 41 deletions(-) create mode 100644 framework/FASTAPI.md create mode 100644 framework/py/flwr/supercore/routers/__init__.py create mode 100644 framework/py/flwr/supercore/routers/health/__init__.py create mode 100644 framework/py/flwr/supercore/routers/health/router.py create mode 100644 framework/py/flwr/superlink/main.py create mode 100644 framework/py/flwr/superlink/routers/__init__.py create mode 100644 framework/py/flwr/superlink/routers/control/__init__.py create mode 100644 framework/py/flwr/superlink/routers/control/router.py create mode 100644 framework/py/flwr/superlink/routers/runtime/__init__.py create mode 100644 framework/py/flwr/superlink/routers/runtime/router.py create mode 100644 framework/py/flwr/supernode/main.py create mode 100644 framework/py/flwr/supernode/routers/__init__.py create mode 100644 framework/py/flwr/supernode/routers/runtime/__init__.py create mode 100644 framework/py/flwr/supernode/routers/runtime/router.py diff --git a/framework/FASTAPI.md b/framework/FASTAPI.md new file mode 100644 index 000000000000..f8272f8f092c --- /dev/null +++ b/framework/FASTAPI.md @@ -0,0 +1,29 @@ +# FastAPI + +## Install + +``` +uv sync --all-extras +``` + +## Run + +Start the SuperLink's FastAPI server using uvicorn: + +``` +uv run uvicorn flwr.superlink.main:app +``` + +Start the SuperNode's FastAPI server using uvicorn: + +``` +uv run uvicorn flwr.supernode.main:app +``` + +## Docs + +Docs are available once the SuperLink/SuperNode FastAPI server is running: + +``` +http://127.0.0.1:8000/docs +``` diff --git a/framework/py/flwr/supercore/routers/__init__.py b/framework/py/flwr/supercore/routers/__init__.py new file mode 100644 index 000000000000..e64528ec5931 --- /dev/null +++ b/framework/py/flwr/supercore/routers/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Core API routers.""" diff --git a/framework/py/flwr/supercore/routers/health/__init__.py b/framework/py/flwr/supercore/routers/health/__init__.py new file mode 100644 index 000000000000..bba2f85d8fd8 --- /dev/null +++ b/framework/py/flwr/supercore/routers/health/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Health API router.""" + + +from .router import router + +__all__ = ["router"] diff --git a/framework/py/flwr/supercore/routers/health/router.py b/framework/py/flwr/supercore/routers/health/router.py new file mode 100644 index 000000000000..581a711b7e69 --- /dev/null +++ b/framework/py/flwr/supercore/routers/health/router.py @@ -0,0 +1,32 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Health API router implementation.""" + + +from fastapi import APIRouter + +router = APIRouter(tags=["health"]) + + +@router.get("/health") +def health() -> dict[str, str]: + """Report whether the API server is healthy. + + Returns + ------- + dict[str, str] + A mapping with key ``status`` set to ``"ok"`` when the server is healthy. + """ + return {"status": "ok"} diff --git a/framework/py/flwr/superlink/main.py b/framework/py/flwr/superlink/main.py new file mode 100644 index 000000000000..4794547c451a --- /dev/null +++ b/framework/py/flwr/superlink/main.py @@ -0,0 +1,35 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperLink API.""" + + +from fastapi import FastAPI + +from flwr.supercore.routers import health +from flwr.superlink.routers import control, runtime + +app = FastAPI( + title="SuperLink API", + version="1.32.0", + docs_url="/docs", + redoc_url=None, +) + +# Core APIs +app.include_router(health.router) + +# SuperLink APIs +app.include_router(control.router) +app.include_router(runtime.router) diff --git a/framework/py/flwr/superlink/routers/__init__.py b/framework/py/flwr/superlink/routers/__init__.py new file mode 100644 index 000000000000..b39cd9c1b844 --- /dev/null +++ b/framework/py/flwr/superlink/routers/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperLink API routers.""" diff --git a/framework/py/flwr/superlink/routers/control/__init__.py b/framework/py/flwr/superlink/routers/control/__init__.py new file mode 100644 index 000000000000..0966f7ef5155 --- /dev/null +++ b/framework/py/flwr/superlink/routers/control/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Control API router.""" + + +from .router import router + +__all__ = ["router"] diff --git a/framework/py/flwr/superlink/routers/control/router.py b/framework/py/flwr/superlink/routers/control/router.py new file mode 100644 index 000000000000..1a6ac8c5dc0f --- /dev/null +++ b/framework/py/flwr/superlink/routers/control/router.py @@ -0,0 +1,32 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Control API router.""" + + +from fastapi import APIRouter + +router = APIRouter(prefix="/control", tags=["control"]) + + +@router.get("/runs") +def list_runs() -> dict[str, str]: + """List runs. + + Returns + ------- + dict[str, str] + Not yet implemented. + """ + return {"status": "not_implemented"} diff --git a/framework/py/flwr/superlink/routers/runtime/__init__.py b/framework/py/flwr/superlink/routers/runtime/__init__.py new file mode 100644 index 000000000000..69272131b49e --- /dev/null +++ b/framework/py/flwr/superlink/routers/runtime/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Runtime API router.""" + + +from .router import router + +__all__ = ["router"] diff --git a/framework/py/flwr/superlink/routers/runtime/router.py b/framework/py/flwr/superlink/routers/runtime/router.py new file mode 100644 index 000000000000..152208f4a549 --- /dev/null +++ b/framework/py/flwr/superlink/routers/runtime/router.py @@ -0,0 +1,32 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Runtime API router.""" + + +from fastapi import APIRouter + +router = APIRouter(prefix="/runtime", tags=["runtime"]) + + +@router.post("/messages") +def pull_messages() -> dict[str, str]: + """Pull messages. + + Returns + ------- + dict[str, str] + Not yet implemented. + """ + return {"status": "not_implemented"} diff --git a/framework/py/flwr/supernode/main.py b/framework/py/flwr/supernode/main.py new file mode 100644 index 000000000000..487a6e943722 --- /dev/null +++ b/framework/py/flwr/supernode/main.py @@ -0,0 +1,34 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperNode API.""" + + +from fastapi import FastAPI + +from flwr.supercore.routers import health +from flwr.supernode.routers import runtime + +app = FastAPI( + title="SuperNode API", + version="1.32.0", + docs_url="/docs", + redoc_url=None, +) + +# Core APIs +app.include_router(health.router) + +# SuperNode APIs +app.include_router(runtime.router) diff --git a/framework/py/flwr/supernode/routers/__init__.py b/framework/py/flwr/supernode/routers/__init__.py new file mode 100644 index 000000000000..cbf90b90296e --- /dev/null +++ b/framework/py/flwr/supernode/routers/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperNode API routers.""" diff --git a/framework/py/flwr/supernode/routers/runtime/__init__.py b/framework/py/flwr/supernode/routers/runtime/__init__.py new file mode 100644 index 000000000000..69272131b49e --- /dev/null +++ b/framework/py/flwr/supernode/routers/runtime/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Runtime API router.""" + + +from .router import router + +__all__ = ["router"] diff --git a/framework/py/flwr/supernode/routers/runtime/router.py b/framework/py/flwr/supernode/routers/runtime/router.py new file mode 100644 index 000000000000..292e74639f92 --- /dev/null +++ b/framework/py/flwr/supernode/routers/runtime/router.py @@ -0,0 +1,32 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Runtime API router.""" + + +from fastapi import APIRouter + +router = APIRouter(prefix="/runtime", tags=["runtime"]) + + +@router.post("/messages") +def pull_messages() -> dict[str, str]: + """Pull messages for the ClientApp. + + Returns + ------- + dict[str, str] + Message payloads for the authenticated ClientApp. Not yet implemented. + """ + return {"status": "not_implemented"} diff --git a/framework/pyproject.toml b/framework/pyproject.toml index 10f669789b2e..d34d6894f668 100644 --- a/framework/pyproject.toml +++ b/framework/pyproject.toml @@ -68,7 +68,11 @@ simulation = [ "ray==2.55.1; python_version >= '3.11' and python_version < '3.13'", "ray==2.55.1; python_version >= '3.13' and python_version < '3.15' and sys_platform != 'win32'", ] -rest = ["starlette>=0.50.0,<0.51.0", "uvicorn[standard]>=0.40.0,<0.41.0"] +rest = [ + "starlette>=1.3.1,<1.4.0", + "uvicorn[standard]>=0.49.0,<0.50.0", + "fastapi[standard]>=0.138.0,<0.139.0", +] agent = ["trafilatura>=2.1.0,<3.0.0"] [project.scripts] diff --git a/framework/uv.lock b/framework/uv.lock index d5ceb8934ee4..e10d6cf7f681 100644 --- a/framework/uv.lock +++ b/framework/uv.lock @@ -32,6 +32,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -785,6 +794,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] +[[package]] +name = "detect-installer" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, +] + [[package]] name = "devtool" version = "0.0.0" @@ -857,6 +875,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "docopt" version = "0.6.2" @@ -927,6 +954,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", size = 16360, upload-time = "2025-06-09T08:21:35.654Z" }, ] +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -936,6 +976,176 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fastapi" +version = "0.138.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/58/ff455d9fe47c60abadb34b9e05a304b1f05f5ab8000ac01565156b6f5e43/fastapi-0.138.0.tar.gz", hash = "sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061", size = 419240, upload-time = "2026-06-20T01:18:05.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/ff/8496d9847a5fedae775eb49460722d3efaa80487854273e9647ae876218c/fastapi-0.138.0-py3-none-any.whl", hash = "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", size = 126779, upload-time = "2026-06-20T01:18:03.483Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "fastar" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/d0/ee5678346811967b8d096d5d5604e71b50d6bf5a2abfbdb331157e2bbaa9/fastapi_cli-0.0.27.tar.gz", hash = "sha256:1dffb1e40c0c88f2e0171a8a252a2b615c1e63ff8c05626649e4badd6a84336a", size = 23630, upload-time = "2026-06-18T14:48:43.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/ab/0a709f9488fe62647db80f8a277fb0ee62e85adc6746abf477ed373c9eb7/fastapi_cli-0.0.27-py3-none-any.whl", hash = "sha256:2e389a40f318e29fec8cb1e289f267f17c048876fb82dbfa869a10b16740495d", size = 13070, upload-time = "2026-06-18T14:48:44.311Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "detect-installer" }, + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/bf/97d19633c6ec6fb0ef59df474b9705ea992f7b4f879208d0007ac6d25ab6/fastapi_cloud_cli-0.20.0.tar.gz", hash = "sha256:9681c46adcd299024d0775658bd5d88992fd35c4ad42b1f045c6df913390ba37", size = 85904, upload-time = "2026-06-11T17:41:02.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/6e/bbb2e1b8f3170b6426b707d49981a838fc1d5cbb428dd9a271f1c3951c23/fastapi_cloud_cli-0.20.0-py3-none-any.whl", hash = "sha256:dcbf071fc659ae2d3fb30e221a661c3fa240b7d5091203cf941face31f6d7860", size = 68793, upload-time = "2026-06-11T17:41:01.804Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7a/fb367bdaf4efa2c7952a45aeab2e87a564293ecffe150af673ec8edfda46/fastar-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b82fd6f996e65a86f67a6bd64dd22ef3e8ae2dcaed0ae3b550e71f7e1bbb1df5", size = 709869, upload-time = "2026-04-13T17:09:55.62Z" }, + { url = "https://files.pythonhosted.org/packages/80/ff/b87efb0dcfd081c62c7c7601d7681dabe63103cd51fc16f8d57a1ab45961/fastar-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27eed386fd0558e6daa29211111bbd7b740f7c7e881197f8a00ac7c0f3cdb1d7", size = 631668, upload-time = "2026-04-13T17:09:40.537Z" }, + { url = "https://files.pythonhosted.org/packages/24/7c/0ed6dd38b9adc04b3a8ec3b7045908e7c2170ba0ff6e6d2c51bc9fc770f3/fastar-0.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a6931bebc1d8e95ddeef55732c195449e6b44ef33aa31b325505097ed3b4d6aa", size = 869663, upload-time = "2026-04-13T17:09:09.78Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/8b7fb3f23855accebaaf2d2637eac7f261a7a5d936f861a172079f1ef511/fastar-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f72ce42a5e28a74fbd4d5fbf1a3ac1a1163d13cbc200cbd005fb0fabc54bd", size = 762938, upload-time = "2026-04-13T17:07:54.51Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/5491e2b677bb841f768e3aba052d0344338a5c78aa5d4c18b443831a8e8d/fastar-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5b83c1f61f7017d6e1498568038f8745440cfc16ca2f697ec81bac83050108f6", size = 759232, upload-time = "2026-04-13T17:08:08.864Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/643630bdbd179e41e9fae31c03b4cf6061dbf4d6fbbae8425d16eb12545d/fastar-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db73a9b765a516e73983b25341e7b5e0189733878279e278b2295131b0e3a21e", size = 926271, upload-time = "2026-04-13T17:08:23.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/5d/37ade50003b4540e0a53ef100f6692d7ab2ac1122d5acf39920cc09a3e8b/fastar-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:625827d52eb4e8fec942e0233f125ff8010fcf6a67c0a974a8e5f4666b771e3c", size = 818634, upload-time = "2026-04-13T17:08:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ff/135d177de32cc1e837c99019e4643e6e79352bde49544d4ece5b5eebf56b/fastar-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5fd8fa21ec0a88296a38dc5d7fc35efd3b26d46a17b8b7c73c5563925ca15", size = 822755, upload-time = "2026-04-13T17:09:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/27/cb/b835dbe76ceac7fa6105851468c259ffd06830eb9c029402e499d0ec153b/fastar-0.11.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8c15af91b8cd87ddf23ea55355ae513c1de3ab67178f26dad017c9e9c0af6096", size = 887101, upload-time = "2026-04-13T17:08:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/9e/54/aa8289eb57fc550535470397cb051f5a58a7c89ca4de31d5502b916dd894/fastar-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a112395a8b0bff251423bd1564c012f0cc058ad8b6bd8fba96f3d7fc117e44", size = 973606, upload-time = "2026-04-13T17:10:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/776d50a0897c01dc6bfd0926772ee913436fdae91b9affaf0a0cbd09f0a1/fastar-0.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2994bb8f5f8c11eb12beae1e6e77a907173c9819236b8a4c8f0573652ceccce", size = 1036696, upload-time = "2026-04-13T17:10:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/cf0f9b499fb37ac065c8a01ec642f96a3c5eb849c38ae983b59f3b3245e0/fastar-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dcf99e4b5973d842c7f19c776c3a83cdc0977d505edce6206438505c0456b517", size = 1078182, upload-time = "2026-04-13T17:10:45.318Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9e/21e4701aec4a1123d4dc4d31578dc18875582b5710e4725f7ceb752a248b/fastar-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29c9c386dc0d5dda78845a8e6b1480d26ab861c1e0b68f42ae5735cb70ca07f1", size = 1032336, upload-time = "2026-04-13T17:11:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/5872b28c72c27ec1a00760eace6ff35f714f41ebbd5208cf016b12e29250/fastar-0.11.0-cp311-cp311-win32.whl", hash = "sha256:030b2580fc394f2c9b7890b6735810404e9b9ed5e0344db150b945965b5482b7", size = 457368, upload-time = "2026-04-13T17:11:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6e/ce6832a16193eb4466f4108be8809c249b51cb1f89dd7894545700d079d5/fastar-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:83ab57ae067969cd0b483ac3b6dccc4b595fc77f5c820760998648d4c42822b5", size = 488605, upload-time = "2026-04-13T17:11:29.161Z" }, + { url = "https://files.pythonhosted.org/packages/15/5a/9cfb80661cf38fd7b0889224beb7d2746784d4ade2a931ed9775a18d8602/fastar-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:27b1a4cee2298b704de8151d310462ee7335ed036011ca9aa6e784b30b6c73a9", size = 464580, upload-time = "2026-04-13T17:11:18.583Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, + { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, + { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, + { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, + { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, + { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/9bbeffbf1905391446dd98aa520422ce7affde5c9a7c22d757cc5d7c1397/fastar-0.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1266d6a004f427b0d61bd6c7b544d84cc964691b2232c2f4d635a1b75f2f6d5e", size = 711644, upload-time = "2026-04-13T17:10:07.663Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/ae5cf39d4fb82d0c592705f5ec6db1b065be5265c151b108f86126ee8773/fastar-0.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:298a827ec04ade43733f6ca960d0faec38706aa1494175869ea7ea17f5bad5d3", size = 634371, upload-time = "2026-04-13T17:09:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/7e/36/8d4569e26473c72ccb02d1c5df3ed710073f1c06eca09c26d52ea79fd815/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8800e2387e463a0e5799416a1cbe72dd0fde7270a20e4bde684145e7878f6516", size = 870850, upload-time = "2026-04-13T17:09:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/724dc796e1756d3977970f820d30d59bb8cab8e3671b285f1d82ab513aec/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7496def0a2befd82d429cb004ef7ca831585cc887947bd6b9abb68a5ef852b0b", size = 764469, upload-time = "2026-04-13T17:08:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/99/e3/74d6859e632e8fb9339a14f652fb9f800c2bd6aa53071e311c0be3fbab8b/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:878eaf15463eb572e3538af7ca3a8534e5e279cf8196db902d24e5725c4af86e", size = 761375, upload-time = "2026-04-13T17:08:20.669Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e7/cc70e2be5ef8731a7525552b1c35c1448cf9eae6a62cb3a56f12c1bf27ea/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0324ed1d1ef0186e1bbd843b17807d6d837d0906899d4c99378b02c5d86bdd9c", size = 928189, upload-time = "2026-04-13T17:08:35.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/33/c9a969e78dca323547276a6fee5f4f9588f7cd5ab45acec3778c67399589/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdf9bd863205590beaf8ef6e66f315310196632180dceaf674985d01a876cac3", size = 820864, upload-time = "2026-04-13T17:09:06.366Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/6b9434b541fe55c125b5f2e017a565596a2d215aa09207e4555e4585064f/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59af8dbb683b24b90fb5b506de080faeab0a17a908e6c2a5d93a97260ed75d7b", size = 824060, upload-time = "2026-04-13T17:09:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/24/8d/871d5f8cf4c6f13987119fb0a9ae8be131e34f2756c2524e9974adf33824/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:9f3df73a3c4292cfe15696cdf59cdb6c309ab59d30b34c733be13c6e32d9a264", size = 889217, upload-time = "2026-04-13T17:08:50.884Z" }, + { url = "https://files.pythonhosted.org/packages/d0/26/cca0fd2704f3ed20165e5613ed911549aef3aaf3b0b5b02fee0e8e23e6cc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa3762cbb16e41a76b61f4a6914937a71aab3a7b6c2d82ca233bc686ebaf756b", size = 975418, upload-time = "2026-04-13T17:10:24.307Z" }, + { url = "https://files.pythonhosted.org/packages/99/94/8bbb0b13f5b6cbe2492f0b7cbba5103e6163976a3331466d010e781fa189/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a8c7bc8ac74cb359bb546b199288c83236372d094b402e557c197e85527495cd", size = 1038492, upload-time = "2026-04-13T17:10:41.939Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d3/5b7df222a30eac2822ffd00f82fd4c2ce84fba4b369d1e1a03732fd177fc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:587cbd060a2699c5f66281081395bb4657b2b1e0eef5c206b1aabf740019d670", size = 1080210, upload-time = "2026-04-13T17:10:58.462Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/56ef943ea524784598c035ccbd42e564e937da0438ae3f55f0e76cb95571/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a1c56957ac82408be37a3f63594bc83e0919e8760492a4475e542f9f1828778", size = 1034886, upload-time = "2026-04-13T17:11:15.617Z" }, +] + [[package]] name = "fastjsonschema" version = "2.21.2" @@ -998,6 +1208,7 @@ agent = [ { name = "trafilatura" }, ] rest = [ + { name = "fastapi", extra = ["standard"] }, { name = "starlette" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -1066,6 +1277,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.18.1,<2.0.0" }, { name = "click", specifier = ">=8.0.0,<9.0.0" }, { name = "cryptography", specifier = ">=46.0.7,<47.0.0" }, + { name = "fastapi", extras = ["standard"], marker = "extra == 'rest'", specifier = ">=0.138.0,<0.139.0" }, { name = "grpcio", specifier = ">=1.70.0,<2.0.0" }, { name = "grpcio-health-checking", specifier = ">=1.70.0,<2.0.0" }, { name = "iterators", specifier = ">=0.0.2,<0.0.3" }, @@ -1080,13 +1292,13 @@ requires-dist = [ { name = "requests", specifier = ">=2.33.0,<3.0.0" }, { name = "rich", specifier = ">=13.5.0,<14.0.0" }, { name = "sqlalchemy", specifier = ">=2.0.45,<3.0.0" }, - { name = "starlette", marker = "extra == 'rest'", specifier = ">=0.50.0,<0.51.0" }, + { name = "starlette", marker = "extra == 'rest'", specifier = ">=1.3.1,<1.4.0" }, { name = "tomli", specifier = ">=2.0.1,<3.0.0" }, { name = "tomli-w", specifier = ">=1.0.0,<2.0.0" }, { name = "trafilatura", marker = "extra == 'agent'", specifier = ">=2.1.0,<3.0.0" }, { name = "typer", specifier = ">=0.13.0,<0.21.0" }, { name = "uv", specifier = ">=0.11.15,<0.12.0" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'rest'", specifier = ">=0.40.0,<0.41.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'rest'", specifier = ">=0.49.0,<0.50.0" }, ] provides-extras = ["simulation", "rest", "agent"] @@ -1385,38 +1597,45 @@ wheels = [ [[package]] name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, - { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, ] [[package]] @@ -2986,6 +3205,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.41.5" @@ -3083,6 +3307,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + [[package]] name = "pydot" version = "4.0.1" @@ -3309,6 +3560,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -3809,6 +4069,115 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] +[[package]] +name = "rich-toolkit" +version = "0.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/63/3e427c62f1992945c997d4ec31e2fcb37d26aadbe5aa44ae5b29f7f64d26/rich_toolkit-0.20.1.tar.gz", hash = "sha256:c7336ae281f435c785acecaedc4b71d4b663dc73d9c8079fea96372527e822a4", size = 203473, upload-time = "2026-06-05T08:56:57.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/88/309f07d08155da2ba1d5ceb42d270fb42fbe34a807684543e3ffc10fe713/rich_toolkit-0.20.1-py3-none-any.whl", hash = "sha256:2a6d5f8e15759b9eba5a9ee63da10b275359ead20e5a0fc92bd5b4dbae8ce4bf", size = 35525, upload-time = "2026-06-05T08:56:58.586Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, +] + [[package]] name = "roman" version = "5.2" @@ -4004,6 +4373,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] +[[package]] +name = "sentry-sdk" +version = "2.63.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/c8/b3c970a5b186722d276cd40a05b3254e03bccc0208560aff20f612e018e8/sentry_sdk-2.63.0.tar.gz", hash = "sha256:2a1502bf864769275dbc8c2c9fc7a0f7f5e18358180b615d262d13a31ffba216", size = 912449, upload-time = "2026-06-16T12:45:57.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/57/cb205f7d93373120f666b9c5736dc0815524d96a9b278e7a728f018dc22a/sentry_sdk-2.63.0-py3-none-any.whl", hash = "sha256:3a9b5ddd403f79eb73bd670f75f04485819db53d28f76ced7bc09041cb0dfd6a", size = 495950, upload-time = "2026-06-16T12:45:55.819Z" }, +] + [[package]] name = "setuptools" version = "82.0.0" @@ -4343,15 +4725,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.50.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] @@ -4738,15 +5120,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, ] [package.optional-dependencies] From f1d7292c22c8a91ae4e7f8b34172aa9793d38792 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Sun, 21 Jun 2026 11:23:44 +0200 Subject: [PATCH 02/33] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- framework/py/flwr/superlink/main.py | 3 ++- framework/py/flwr/supernode/main.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/framework/py/flwr/superlink/main.py b/framework/py/flwr/superlink/main.py index 4794547c451a..b6d52da34ce3 100644 --- a/framework/py/flwr/superlink/main.py +++ b/framework/py/flwr/superlink/main.py @@ -17,12 +17,13 @@ from fastapi import FastAPI +from flwr import __version__ from flwr.supercore.routers import health from flwr.superlink.routers import control, runtime app = FastAPI( title="SuperLink API", - version="1.32.0", + version=__version__, docs_url="/docs", redoc_url=None, ) diff --git a/framework/py/flwr/supernode/main.py b/framework/py/flwr/supernode/main.py index 487a6e943722..62772a00cff7 100644 --- a/framework/py/flwr/supernode/main.py +++ b/framework/py/flwr/supernode/main.py @@ -17,12 +17,13 @@ from fastapi import FastAPI +from flwr import __version__ from flwr.supercore.routers import health from flwr.supernode.routers import runtime app = FastAPI( title="SuperNode API", - version="1.32.0", + version=__version__, docs_url="/docs", redoc_url=None, ) From c84ad18197f73c2e265e9060293f98df5b7297a2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Sun, 21 Jun 2026 13:55:46 +0200 Subject: [PATCH 03/33] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- framework/FASTAPI.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/FASTAPI.md b/framework/FASTAPI.md index f8272f8f092c..aaa49b890a1f 100644 --- a/framework/FASTAPI.md +++ b/framework/FASTAPI.md @@ -2,9 +2,9 @@ ## Install -``` -uv sync --all-extras -``` +~~~ +uv sync --locked --all-extras +~~~ ## Run From a15055a66513d614c1dc78f961796c3105711d4f Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Sun, 21 Jun 2026 13:57:19 +0200 Subject: [PATCH 04/33] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- framework/py/flwr/superlink/routers/control/router.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/framework/py/flwr/superlink/routers/control/router.py b/framework/py/flwr/superlink/routers/control/router.py index 1a6ac8c5dc0f..68253b5a58b0 100644 --- a/framework/py/flwr/superlink/routers/control/router.py +++ b/framework/py/flwr/superlink/routers/control/router.py @@ -20,6 +20,11 @@ router = APIRouter(prefix="/control", tags=["control"]) +from fastapi import APIRouter, HTTPException + +router = APIRouter(prefix="/control", tags=["control"]) + + @router.get("/runs") def list_runs() -> dict[str, str]: """List runs. @@ -29,4 +34,4 @@ def list_runs() -> dict[str, str]: dict[str, str] Not yet implemented. """ - return {"status": "not_implemented"} + raise HTTPException(status_code=501, detail="Not implemented") From d4d0873fb230cb7a6a1ea22cbab87b63168f318e Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Sun, 21 Jun 2026 13:57:37 +0200 Subject: [PATCH 05/33] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- framework/py/flwr/superlink/routers/runtime/router.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/framework/py/flwr/superlink/routers/runtime/router.py b/framework/py/flwr/superlink/routers/runtime/router.py index 152208f4a549..2a42e3b01b72 100644 --- a/framework/py/flwr/superlink/routers/runtime/router.py +++ b/framework/py/flwr/superlink/routers/runtime/router.py @@ -20,6 +20,11 @@ router = APIRouter(prefix="/runtime", tags=["runtime"]) +from fastapi import APIRouter, HTTPException + +router = APIRouter(prefix="/runtime", tags=["runtime"]) + + @router.post("/messages") def pull_messages() -> dict[str, str]: """Pull messages. @@ -29,4 +34,4 @@ def pull_messages() -> dict[str, str]: dict[str, str] Not yet implemented. """ - return {"status": "not_implemented"} + raise HTTPException(status_code=501, detail="Not implemented") From 25d0bf8023f20015bfe16f05ca64022c854f76da Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Sun, 21 Jun 2026 13:58:08 +0200 Subject: [PATCH 06/33] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- framework/py/flwr/supernode/routers/runtime/router.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/framework/py/flwr/supernode/routers/runtime/router.py b/framework/py/flwr/supernode/routers/runtime/router.py index 292e74639f92..4d49d2c32205 100644 --- a/framework/py/flwr/supernode/routers/runtime/router.py +++ b/framework/py/flwr/supernode/routers/runtime/router.py @@ -20,6 +20,11 @@ router = APIRouter(prefix="/runtime", tags=["runtime"]) +from fastapi import APIRouter, HTTPException + +router = APIRouter(prefix="/runtime", tags=["runtime"]) + + @router.post("/messages") def pull_messages() -> dict[str, str]: """Pull messages for the ClientApp. @@ -29,4 +34,4 @@ def pull_messages() -> dict[str, str]: dict[str, str] Message payloads for the authenticated ClientApp. Not yet implemented. """ - return {"status": "not_implemented"} + raise HTTPException(status_code=501, detail="Not implemented") From 7356ba6372dbac8e285f8060eb2ef33ef709bab7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Sun, 21 Jun 2026 15:36:33 +0200 Subject: [PATCH 07/33] Fix imports --- framework/py/flwr/superlink/routers/control/router.py | 6 ------ framework/py/flwr/superlink/routers/runtime/router.py | 6 ------ framework/py/flwr/supernode/routers/runtime/router.py | 6 ------ 3 files changed, 18 deletions(-) diff --git a/framework/py/flwr/superlink/routers/control/router.py b/framework/py/flwr/superlink/routers/control/router.py index 68253b5a58b0..ea90c961e274 100644 --- a/framework/py/flwr/superlink/routers/control/router.py +++ b/framework/py/flwr/superlink/routers/control/router.py @@ -14,12 +14,6 @@ # ============================================================================== """Control API router.""" - -from fastapi import APIRouter - -router = APIRouter(prefix="/control", tags=["control"]) - - from fastapi import APIRouter, HTTPException router = APIRouter(prefix="/control", tags=["control"]) diff --git a/framework/py/flwr/superlink/routers/runtime/router.py b/framework/py/flwr/superlink/routers/runtime/router.py index 2a42e3b01b72..b43767f95e8f 100644 --- a/framework/py/flwr/superlink/routers/runtime/router.py +++ b/framework/py/flwr/superlink/routers/runtime/router.py @@ -14,12 +14,6 @@ # ============================================================================== """Runtime API router.""" - -from fastapi import APIRouter - -router = APIRouter(prefix="/runtime", tags=["runtime"]) - - from fastapi import APIRouter, HTTPException router = APIRouter(prefix="/runtime", tags=["runtime"]) diff --git a/framework/py/flwr/supernode/routers/runtime/router.py b/framework/py/flwr/supernode/routers/runtime/router.py index 4d49d2c32205..5184e1d07a3d 100644 --- a/framework/py/flwr/supernode/routers/runtime/router.py +++ b/framework/py/flwr/supernode/routers/runtime/router.py @@ -14,12 +14,6 @@ # ============================================================================== """Runtime API router.""" - -from fastapi import APIRouter - -router = APIRouter(prefix="/runtime", tags=["runtime"]) - - from fastapi import APIRouter, HTTPException router = APIRouter(prefix="/runtime", tags=["runtime"]) From 7deab810ad13496fc36162f1335bbc5514cd5c63 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Tue, 23 Jun 2026 21:08:02 +0200 Subject: [PATCH 08/33] Introduce create_app() --- framework/py/flwr/superlink/main.py | 34 ++++++++++++++++++----------- framework/py/flwr/supernode/main.py | 32 +++++++++++++++++---------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/framework/py/flwr/superlink/main.py b/framework/py/flwr/superlink/main.py index b6d52da34ce3..acb4d17148e5 100644 --- a/framework/py/flwr/superlink/main.py +++ b/framework/py/flwr/superlink/main.py @@ -21,16 +21,24 @@ from flwr.supercore.routers import health from flwr.superlink.routers import control, runtime -app = FastAPI( - title="SuperLink API", - version=__version__, - docs_url="/docs", - redoc_url=None, -) - -# Core APIs -app.include_router(health.router) - -# SuperLink APIs -app.include_router(control.router) -app.include_router(runtime.router) + +def create_app() -> FastAPI: + """Create the SuperLink FastAPI app.""" + app = FastAPI( + title="SuperLink API", + version=__version__, + docs_url="/docs", + redoc_url=None, + ) + + # Core APIs + app.include_router(health.router) + + # SuperLink APIs + app.include_router(control.router) + app.include_router(runtime.router) + + return app + + +app = create_app() diff --git a/framework/py/flwr/supernode/main.py b/framework/py/flwr/supernode/main.py index 62772a00cff7..19ef0b3e8345 100644 --- a/framework/py/flwr/supernode/main.py +++ b/framework/py/flwr/supernode/main.py @@ -21,15 +21,23 @@ from flwr.supercore.routers import health from flwr.supernode.routers import runtime -app = FastAPI( - title="SuperNode API", - version=__version__, - docs_url="/docs", - redoc_url=None, -) - -# Core APIs -app.include_router(health.router) - -# SuperNode APIs -app.include_router(runtime.router) + +def create_app() -> FastAPI: + """Create the SuperNode FastAPI app.""" + app = FastAPI( + title="SuperNode API", + version=__version__, + docs_url="/docs", + redoc_url=None, + ) + + # Core APIs + app.include_router(health.router) + + # SuperNode APIs + app.include_router(runtime.router) + + return app + + +app = create_app() From 5f3f62e3e4a37a9ad5728ff9fc06d00e2b2f89f8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Tue, 23 Jun 2026 21:14:59 +0200 Subject: [PATCH 09/33] Add lifespan --- framework/py/flwr/superlink/main.py | 18 ++++++++++++++++++ framework/py/flwr/supernode/main.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/framework/py/flwr/superlink/main.py b/framework/py/flwr/superlink/main.py index acb4d17148e5..8b941ea339c5 100644 --- a/framework/py/flwr/superlink/main.py +++ b/framework/py/flwr/superlink/main.py @@ -15,20 +15,38 @@ """SuperLink API.""" +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from logging import INFO + from fastapi import FastAPI from flwr import __version__ +from flwr.common import log from flwr.supercore.routers import health from flwr.superlink.routers import control, runtime def create_app() -> FastAPI: """Create the SuperLink FastAPI app.""" + + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncIterator[None]: + """Own process-lifetime resources for the combined SuperLink service.""" + log(INFO, "FastAPI lifespan: startup") + + yield + + log(INFO, "FastAPI lifespan: shutdown") + app = FastAPI( title="SuperLink API", version=__version__, docs_url="/docs", redoc_url=None, + lifespan=lifespan, ) # Core APIs diff --git a/framework/py/flwr/supernode/main.py b/framework/py/flwr/supernode/main.py index 19ef0b3e8345..4d39151837aa 100644 --- a/framework/py/flwr/supernode/main.py +++ b/framework/py/flwr/supernode/main.py @@ -15,20 +15,38 @@ """SuperNode API.""" +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from logging import INFO + from fastapi import FastAPI from flwr import __version__ +from flwr.common import log from flwr.supercore.routers import health from flwr.supernode.routers import runtime def create_app() -> FastAPI: """Create the SuperNode FastAPI app.""" + + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncIterator[None]: + """Own process-lifetime resources for the combined SuperLink service.""" + log(INFO, "FastAPI lifespan: startup") + + yield + + log(INFO, "FastAPI lifespan: shutdown") + app = FastAPI( title="SuperNode API", version=__version__, docs_url="/docs", redoc_url=None, + lifespan=lifespan, ) # Core APIs From 5f0002b8ff20e7bcdbe5dd8ac84d4a45bd82a7ea Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Tue, 23 Jun 2026 22:02:28 +0200 Subject: [PATCH 10/33] refactor(framework): Move flower-superlink and flwr-serverapp entrypoints --- framework/docs/source/ref-api-cli.rst | 2 +- .../fleet/grpc_rere/fleet_servicer_test.py | 2 +- .../node_auth_server_interceptor_test.py | 2 +- framework/py/flwr/superlink/cli/__init__.py | 24 ++++++ .../cli/flower_superlink.py} | 19 ++--- .../cli/flower_superlink_test.py} | 12 +-- .../py/flwr/superlink/cli/flwr_serverapp.py | 74 +++++++++++++++++++ .../cli/flwr_serverapp_test.py} | 4 +- .../runtime}/__init__.py | 6 +- .../runtime/run_serverapp.py} | 70 +----------------- framework/pyproject.toml | 4 +- 11 files changed, 129 insertions(+), 90 deletions(-) create mode 100644 framework/py/flwr/superlink/cli/__init__.py rename framework/py/flwr/{server/app.py => superlink/cli/flower_superlink.py} (98%) rename framework/py/flwr/{server/app_test.py => superlink/cli/flower_superlink_test.py} (97%) create mode 100644 framework/py/flwr/superlink/cli/flwr_serverapp.py rename framework/py/flwr/{server/serverapp/app_test.py => superlink/cli/flwr_serverapp_test.py} (96%) rename framework/py/flwr/{server/serverapp => superlink/runtime}/__init__.py (87%) rename framework/py/flwr/{server/serverapp/app.py => superlink/runtime/run_serverapp.py} (80%) diff --git a/framework/docs/source/ref-api-cli.rst b/framework/docs/source/ref-api-cli.rst index 0650c45c8261..cbc572ad9083 100644 --- a/framework/docs/source/ref-api-cli.rst +++ b/framework/docs/source/ref-api-cli.rst @@ -21,7 +21,7 @@ ==================== .. argparse:: - :module: flwr.server.app + :module: flwr.superlink.cli.flower_superlink :func: _parse_args_run_superlink :prog: flower-superlink diff --git a/framework/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer_test.py b/framework/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer_test.py index 5ef3d476486c..c46bded3c69f 100644 --- a/framework/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer_test.py +++ b/framework/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer_test.py @@ -58,7 +58,6 @@ ) from flwr.proto.node_pb2 import Node # pylint: disable=E0611 from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611 -from flwr.server.app import _run_fleet_api_grpc_rere from flwr.server.superlink.linkstate.linkstate_factory import LinkStateFactory from flwr.server.superlink.linkstate.linkstate_test import ( create_ins_message, @@ -79,6 +78,7 @@ iterate_object_tree, ) from flwr.supercore.object_store import ObjectStoreFactory +from flwr.superlink.cli.flower_superlink import _run_fleet_api_grpc_rere from flwr.superlink.federation import NoOpFederationManager diff --git a/framework/py/flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor_test.py b/framework/py/flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor_test.py index 0d924a05020b..34c99cb843d9 100644 --- a/framework/py/flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor_test.py +++ b/framework/py/flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor_test.py @@ -63,7 +63,6 @@ ) from flwr.proto.node_pb2 import Node # pylint: disable=E0611 from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611 -from flwr.server.app import _run_fleet_api_grpc_rere from flwr.server.superlink.linkstate.linkstate_factory import LinkStateFactory from flwr.server.superlink.linkstate.linkstate_test import create_res_message from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME, NOOP_FEDERATION, TaskType @@ -75,6 +74,7 @@ public_key_to_bytes, sign_message, ) +from flwr.superlink.cli.flower_superlink import _run_fleet_api_grpc_rere from flwr.superlink.federation import NoOpFederationManager from .node_auth_server_interceptor import NodeAuthServerInterceptor diff --git a/framework/py/flwr/superlink/cli/__init__.py b/framework/py/flwr/superlink/cli/__init__.py new file mode 100644 index 000000000000..1c13e7e217bc --- /dev/null +++ b/framework/py/flwr/superlink/cli/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2025 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Flower command line interface for SuperLink.""" + + +from .flower_superlink import flower_superlink +from .flwr_serverapp import flwr_serverapp + +__all__ = [ + "flower_superlink", + "flwr_serverapp", +] diff --git a/framework/py/flwr/server/app.py b/framework/py/flwr/superlink/cli/flower_superlink.py similarity index 98% rename from framework/py/flwr/server/app.py rename to framework/py/flwr/superlink/cli/flower_superlink.py index 71be17b24599..c42021d8751c 100644 --- a/framework/py/flwr/server/app.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"""Flower server app.""" +"""`flower-superlink` command.""" import argparse @@ -59,6 +59,14 @@ ) from flwr.proto.grpcadapter_pb2_grpc import add_GrpcAdapterServicer_to_server from flwr.server.fleet_event_log_interceptor import FleetEventLogInterceptor +from flwr.server.superlink.fleet.grpc_adapter.grpc_adapter_servicer import ( + GrpcAdapterServicer, +) +from flwr.server.superlink.fleet.grpc_rere.fleet_servicer import FleetServicer +from flwr.server.superlink.fleet.grpc_rere.node_auth_server_interceptor import ( + NodeAuthServerInterceptor, +) +from flwr.server.superlink.linkstate import LinkStateFactory from flwr.supercore.address import parse_address, resolve_bind_address from flwr.supercore.auth import ( add_superexec_auth_secret_args, @@ -88,13 +96,6 @@ from flwr.superlink.servicer.control import run_control_api_grpc from flwr.superlink.servicer.serverappio import run_serverappio_api_grpc -from .superlink.fleet.grpc_adapter.grpc_adapter_servicer import GrpcAdapterServicer -from .superlink.fleet.grpc_rere.fleet_servicer import FleetServicer -from .superlink.fleet.grpc_rere.node_auth_server_interceptor import ( - NodeAuthServerInterceptor, -) -from .superlink.linkstate import LinkStateFactory - P = TypeVar("P", ControlAuthnPlugin, ControlAuthzPlugin) @@ -209,7 +210,7 @@ def _get_objectstore_linkstate_factories( # pylint: disable=too-many-branches, too-many-locals, too-many-statements -def run_superlink() -> None: +def flower_superlink() -> None: """Run Flower SuperLink (ServerAppIo API and Fleet API).""" warn_if_flwr_update_available(process_name="flower-superlink") diff --git a/framework/py/flwr/server/app_test.py b/framework/py/flwr/superlink/cli/flower_superlink_test.py similarity index 97% rename from framework/py/flwr/server/app_test.py rename to framework/py/flwr/superlink/cli/flower_superlink_test.py index 5aa4bf8a0aff..50d2f153e7f2 100644 --- a/framework/py/flwr/server/app_test.py +++ b/framework/py/flwr/superlink/cli/flower_superlink_test.py @@ -16,6 +16,7 @@ import argparse +import importlib from types import SimpleNamespace from unittest.mock import Mock, patch @@ -23,15 +24,16 @@ import pytest from flwr.common.constant import FLWR_DISABLE_RUNTIME_DEPENDENCY_INSTALLATION +from flwr.server.superlink.linkstate import LinkStateFactory from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME from flwr.supercore.interceptors import RuntimeVersionServerInterceptor from flwr.supercore.object_store import ObjectStoreFactory from flwr.supercore.version import package_version from flwr.superlink.federation import NoOpFederationManager -from . import app as app_module -from .app import _obtain_superlink_certificates, _parse_args_run_superlink -from .superlink.linkstate import LinkStateFactory +from .flower_superlink import _obtain_superlink_certificates, _parse_args_run_superlink + +app_module = importlib.import_module("flwr.superlink.cli.flower_superlink") def test_parse_superlink_log_rotation_args_defaults() -> None: @@ -134,7 +136,7 @@ def test_parse_superlink_log_rotation_backup_requires_positive_int( _parse_args_run_superlink().parse_args(["--log-rotation-backup-count", value]) -def test_run_superlink_checks_for_update(monkeypatch: pytest.MonkeyPatch) -> None: +def test_flower_superlink_checks_for_update(monkeypatch: pytest.MonkeyPatch) -> None: """SuperLink should run the startup update check before parsing arguments.""" class _SentinelError(Exception): @@ -164,7 +166,7 @@ def _unexpected_parse_args() -> _Parser: monkeypatch.setattr(app_module, "warn_if_flwr_update_available", _raise_sentinel) with pytest.raises(_SentinelError): - app_module.run_superlink() + app_module.flower_superlink() assert captured == ["update", "flower-superlink"] diff --git a/framework/py/flwr/superlink/cli/flwr_serverapp.py b/framework/py/flwr/superlink/cli/flwr_serverapp.py new file mode 100644 index 000000000000..b09f39f917bb --- /dev/null +++ b/framework/py/flwr/superlink/cli/flwr_serverapp.py @@ -0,0 +1,74 @@ +# Copyright 2025 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""`flwr-serverapp` command.""" + + +import argparse +from logging import DEBUG, INFO +from queue import Queue + +from flwr.common.args import add_args_flwr_app_common, try_obtain_flwr_app_token +from flwr.common.constant import SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS +from flwr.common.logger import log, mirror_output_to_queue, restore_output +from flwr.supercore.tls import validate_and_resolve_root_certificates +from flwr.superlink.runtime import run_serverapp + + +def flwr_serverapp() -> None: + """Run process-isolated Flower ServerApp.""" + args = _parse_args_run_flwr_serverapp().parse_args() + token = try_obtain_flwr_app_token(args) + + # Capture stdout/stderr + log_queue: Queue[str | None] = Queue() + mirror_output_to_queue(log_queue) + + log(INFO, "Start `flwr-serverapp` process") + log( + DEBUG, + "`flwr-serverapp` will attempt to connect to SuperLink's " + "ServerAppIo API at %s", + args.serverappio_api_address, + ) + run_serverapp( + serverappio_api_address=args.serverappio_api_address, + log_queue=log_queue, + token=token, + insecure=args.insecure, + certificates=validate_and_resolve_root_certificates( + args.root_certificates, args.insecure + ), + parent_pid=args.parent_pid, + runtime_dependency_install=args.runtime_dependency_install, + ) + + # Restore stdout/stderr + restore_output() + + +def _parse_args_run_flwr_serverapp() -> argparse.ArgumentParser: + """Parse flwr-serverapp command line arguments.""" + parser = argparse.ArgumentParser( + description="Run a Flower ServerApp", + ) + parser.add_argument( + "--serverappio-api-address", + default=SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS, + type=str, + help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name)." + f"By default, it is set to {SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS}.", + ) + add_args_flwr_app_common(parser=parser) + return parser diff --git a/framework/py/flwr/server/serverapp/app_test.py b/framework/py/flwr/superlink/cli/flwr_serverapp_test.py similarity index 96% rename from framework/py/flwr/server/serverapp/app_test.py rename to framework/py/flwr/superlink/cli/flwr_serverapp_test.py index 6fb7a0be899b..fc08e248a35e 100644 --- a/framework/py/flwr/server/serverapp/app_test.py +++ b/framework/py/flwr/superlink/cli/flwr_serverapp_test.py @@ -23,9 +23,9 @@ from flwr.common.constant import SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS -from .app import _parse_args_run_flwr_serverapp +from .flwr_serverapp import _parse_args_run_flwr_serverapp -serverapp_module = importlib.import_module("flwr.server.serverapp.app") +serverapp_module = importlib.import_module("flwr.superlink.cli.flwr_serverapp") def test_parse_flwr_serverapp_requires_token() -> None: diff --git a/framework/py/flwr/server/serverapp/__init__.py b/framework/py/flwr/superlink/runtime/__init__.py similarity index 87% rename from framework/py/flwr/server/serverapp/__init__.py rename to framework/py/flwr/superlink/runtime/__init__.py index 5cbdd1731cb8..98815ff9a3d7 100644 --- a/framework/py/flwr/server/serverapp/__init__.py +++ b/framework/py/flwr/superlink/runtime/__init__.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"""Flower AppIO service.""" +"""Flower ServerApp runtime.""" -from .app import flwr_serverapp as flwr_serverapp +from .run_serverapp import run_serverapp __all__ = [ - "flwr_serverapp", + "run_serverapp", ] diff --git a/framework/py/flwr/server/serverapp/app.py b/framework/py/flwr/superlink/runtime/run_serverapp.py similarity index 80% rename from framework/py/flwr/server/serverapp/app.py rename to framework/py/flwr/superlink/runtime/run_serverapp.py index 49fc242d351e..cda47ef40a15 100644 --- a/framework/py/flwr/server/serverapp/app.py +++ b/framework/py/flwr/superlink/runtime/run_serverapp.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"""Flower ServerApp process.""" +"""Flower ServerApp runtime.""" -import argparse -from logging import DEBUG, ERROR, INFO +from logging import DEBUG, ERROR from pathlib import Path from queue import Queue @@ -27,25 +26,13 @@ from flwr.cli.config_utils import get_fab_metadata from flwr.cli.install import install_from_fab from flwr.cli.utils import get_sha256_hash -from flwr.common.args import add_args_flwr_app_common, try_obtain_flwr_app_token from flwr.common.config import ( get_fused_config_from_dir, get_project_config, get_project_dir, ) -from flwr.common.constant import ( - RUNTIME_DEPENDENCY_INSTALL, - SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS, - SubStatus, -) -from flwr.common.logger import ( - flush_logs, - log, - mirror_output_to_queue, - restore_output, - start_log_uploader, - stop_log_uploader, -) +from flwr.common.constant import RUNTIME_DEPENDENCY_INSTALL, SubStatus +from flwr.common.logger import flush_logs, log, start_log_uploader, stop_log_uploader from flwr.common.serde import ( context_from_proto, context_to_proto, @@ -67,42 +54,9 @@ install_app_dependencies, ) from flwr.supercore.telemetry import EventType, event -from flwr.supercore.tls import validate_and_resolve_root_certificates from flwr.superlink.grid import GrpcGrid -def flwr_serverapp() -> None: - """Run process-isolated Flower ServerApp.""" - args = _parse_args_run_flwr_serverapp().parse_args() - token = try_obtain_flwr_app_token(args) - - # Capture stdout/stderr - log_queue: Queue[str | None] = Queue() - mirror_output_to_queue(log_queue) - - log(INFO, "Start `flwr-serverapp` process") - log( - DEBUG, - "`flwr-serverapp` will attempt to connect to SuperLink's " - "ServerAppIo API at %s", - args.serverappio_api_address, - ) - run_serverapp( - serverappio_api_address=args.serverappio_api_address, - log_queue=log_queue, - token=token, - insecure=args.insecure, - certificates=validate_and_resolve_root_certificates( - args.root_certificates, args.insecure - ), - parent_pid=args.parent_pid, - runtime_dependency_install=args.runtime_dependency_install, - ) - - # Restore stdout/stderr - restore_output() - - def run_serverapp( # pylint: disable=R0912, R0913, R0914, R0915, R0917, W0212 serverappio_api_address: str, log_queue: Queue[str | None], @@ -297,19 +251,3 @@ def on_exit() -> None: "success": exit_code == ExitCode.SUCCESS, }, ) - - -def _parse_args_run_flwr_serverapp() -> argparse.ArgumentParser: - """Parse flwr-serverapp command line arguments.""" - parser = argparse.ArgumentParser( - description="Run a Flower ServerApp", - ) - parser.add_argument( - "--serverappio-api-address", - default=SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS, - type=str, - help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name)." - f"By default, it is set to {SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS}.", - ) - add_args_flwr_app_common(parser=parser) - return parser diff --git a/framework/pyproject.toml b/framework/pyproject.toml index 9abb61c32ec2..8ddc936b3bd1 100644 --- a/framework/pyproject.toml +++ b/framework/pyproject.toml @@ -77,10 +77,10 @@ flwr = "flwr.cli.app:app" # Simulation Engine flwr-simulation = "flwr.simulation.app:flwr_simulation" # Deployment Engine -flower-superlink = "flwr.server.app:run_superlink" +flower-superlink = "flwr.superlink.cli:flower_superlink" flower-supernode = "flwr.supernode.cli:flower_supernode" flower-superexec = "flwr.supercore.cli:flower_superexec" -flwr-serverapp = "flwr.server.serverapp:flwr_serverapp" +flwr-serverapp = "flwr.superlink.cli:flwr_serverapp" flwr-clientapp = "flwr.supernode.cli:flwr_clientapp" flwr-agentapp = "flwr.supercore.cli:flwr_agentapp" flwr-model = "flwr.supercore.cli:flwr_model" From 7c68ae8e566486e2cbafc3eb2c2a3f557731724d Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 12:01:59 +0200 Subject: [PATCH 11/33] Add SuperLinkLifespan scaffold --- .../py/flwr/superlink/cli/flower_superlink.py | 18 +++++++++++ framework/py/flwr/superlink/main.py | 30 +++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index c42021d8751c..55868a8848a5 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -209,6 +209,24 @@ def _get_objectstore_linkstate_factories( return objectstore_factory, state_factory +class SuperLinkLifespan: + """Own the shared SuperLink lifespan state and legacy network servers. + + Long-term, the gRPC-specific parts of this class should shrink until it only + initializes shared services used by FastAPI routers. During the migration, + FastAPI lifespan can use this object to start the existing gRPC APIs as + compatibility adapters. + """ + + def startup(self) -> None: + """Start shared lifespan and legacy SuperLink gRPC servers.""" + log(INFO, "SuperLinkLifespan: start") + + def shutdown(self) -> None: + """Stop legacy gRPC servers started by this lifespan.""" + log(INFO, "SuperLinkLifespan: stop") + + # pylint: disable=too-many-branches, too-many-locals, too-many-statements def flower_superlink() -> None: """Run Flower SuperLink (ServerAppIo API and Fleet API).""" diff --git a/framework/py/flwr/superlink/main.py b/framework/py/flwr/superlink/main.py index 8b941ea339c5..0584dc66eb8c 100644 --- a/framework/py/flwr/superlink/main.py +++ b/framework/py/flwr/superlink/main.py @@ -26,18 +26,42 @@ from flwr import __version__ from flwr.common import log from flwr.supercore.routers import health +from flwr.superlink.cli.flower_superlink import SuperLinkLifespan from flwr.superlink.routers import control, runtime -def create_app() -> FastAPI: - """Create the SuperLink FastAPI app.""" +def create_app( + *, + superlink_lifespan: SuperLinkLifespan | None = None, + start_legacy_grpc: bool = False, +) -> FastAPI: + """Create the SuperLink FastAPI app. + + This FastAPI app can be started in two ways: + 1. Via `flower-superlink`: superlink_lifespan will be passed + 2. Via `uvicorn flwr.superlink.main:app`: superlink_lifespan will be None + """ @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: """Own process-lifetime resources for the combined SuperLink service.""" log(INFO, "FastAPI lifespan: startup") - yield + if superlink_lifespan is not None: + # Store the SuperLinkLifespan where future REST routers can access shared + # state through FastAPI dependencies + app.state.superlink_lifespan = superlink_lifespan + + if superlink_lifespan is not None and start_legacy_grpc: + # Temporary compatibility path: start the existing gRPC APIs from + # FastAPI lifespan + superlink_lifespan.startup() + + try: + yield + finally: + if superlink_lifespan is not None and start_legacy_grpc: + superlink_lifespan.shutdown() log(INFO, "FastAPI lifespan: shutdown") From 3560ff260a93574134deb40d6be8ac86919d0c84 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 12:02:46 +0200 Subject: [PATCH 12/33] Add SuperLinkLifespanConfig --- .../py/flwr/superlink/cli/flower_superlink.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index 55868a8848a5..2e4986f67db6 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -22,6 +22,7 @@ import sys import threading from collections.abc import Callable, Sequence +from dataclasses import dataclass from logging import INFO, WARN from pathlib import Path from time import sleep @@ -209,6 +210,18 @@ def _get_objectstore_linkstate_factories( return objectstore_factory, state_factory +@dataclass +class SuperLinkLifespanConfig: + """Configuration needed to start the SuperLink lifespan.""" + + serverappio_address: str + control_address: str + health_server_address: str | None + fleet_api_type: str + fleet_api_address: str | None + fleet_api_num_workers: int + + class SuperLinkLifespan: """Own the shared SuperLink lifespan state and legacy network servers. @@ -218,6 +231,9 @@ class SuperLinkLifespan: compatibility adapters. """ + def __init__(self, config: SuperLinkLifespanConfig) -> None: + self.config = config + def startup(self) -> None: """Start shared lifespan and legacy SuperLink gRPC servers.""" log(INFO, "SuperLinkLifespan: start") From 0e13b057ad68cc17c2bb89072a8895ee015b11b1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 12:04:10 +0200 Subject: [PATCH 13/33] Start FastAPI via flower-superlink --- .../py/flwr/superlink/cli/flower_superlink.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index 2e4986f67db6..25e0be8cc684 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -99,6 +99,9 @@ P = TypeVar("P", ControlAuthnPlugin, ControlAuthzPlugin) +UVICORN_DEFAULT_HOST = "127.0.0.1" +UVICORN_DEFAULT_PORT = 8000 + try: from flwr.ee import ( @@ -420,6 +423,29 @@ def flower_superlink() -> None: f" to the Flower documentation for more information: {url_v}{page}", ) + lifespan_config = SuperLinkLifespanConfig( + serverappio_address=serverappio_address, + control_address=control_address, + health_server_address=health_server_address, + fleet_api_type=args.fleet_api_type, + fleet_api_address=args.fleet_api_address, + fleet_api_num_workers=args.fleet_api_num_workers, + ) + + ########################################################################### + # Run SuperLink in Compatibility Mode (FastAPI + gRPC) + ########################################################################### + + # Enable this mode by running `flower-superlink --enable-http-api` + if args.enable_http_api: + # Blocking: this will run uvicorn.run() + _run_superlink_http_api(args=args, lifespan_config=lifespan_config) + return + + ########################################################################### + # Run SuperLink in Legacy Mode (Only gRPC) + ########################################################################### + # Load Federation Manager federation_manager = get_federation_manager(is_simulation=args.simulation) @@ -583,6 +609,60 @@ def _format_address(address: str) -> tuple[str, str, int]: return (f"[{host}]:{port}" if is_v6 else f"{host}:{port}", host, port) +def _run_superlink_http_api( + args: argparse.Namespace, lifespan_config: SuperLinkLifespanConfig +) -> None: + """Run the experimental FastAPI-owned SuperLink service. + + In this mode, FastAPI owns process startup and starts the current + gRPC APIs from its lifespan as legacy compatibility adapters. Later, the + REST routers should call shared SuperLink services directly and this runtime + should no longer bind gRPC ports. + """ + if lifespan_config.fleet_api_type == TRANSPORT_TYPE_REST: + flwr_exit( + ExitCode.SUPERLINK_INVALID_ARGS, + "`--enable-http-api` cannot be combined with `--fleet-api-type rest`", + ) + if importlib.util.find_spec("uvicorn") is None: + flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST) + + try: + import uvicorn + + from flwr.superlink.main import create_app + except ModuleNotFoundError: + flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST) + + superlink_lifespan = SuperLinkLifespan(lifespan_config) + fastapi_app = create_app( + superlink_lifespan=superlink_lifespan, + start_legacy_grpc=True, + ) + + log( + WARN, + "EXPERIMENTAL: Starting the combined SuperLink FastAPI service on %s:%s. " + "The legacy gRPC APIs are started from FastAPI lifespan.", + args.host, + args.port, + ) + + # Uvicorn workers must stay at 1 while the lifespan starts gRPC servers. With + # multiple workers, every worker process would try to bind the same Control, + # Fleet, and ServerAppIo ports. + uvicorn.run( + app=fastapi_app, + host=args.host, + port=args.port, + reload=False, + access_log=True, + ssl_keyfile=None if args.insecure else args.ssl_keyfile, + ssl_certfile=None if args.insecure else args.ssl_certfile, + workers=1, + ) + + def _obtain_superlink_certificates( args: argparse.Namespace, ) -> tuple[tuple[bytes, bytes, bytes] | None, tuple[bytes, bytes, bytes] | None]: @@ -835,6 +915,7 @@ def _parse_args_run_superlink() -> argparse.ArgumentParser: _add_args_common(parser=parser) add_ee_args_superlink(parser=parser) + _add_args_http_api(parser=parser) _add_args_serverappio_api(parser=parser) _add_args_fleet_api(parser=parser) _add_args_control_api(parser=parser) @@ -932,6 +1013,35 @@ def _add_args_common(parser: argparse.ArgumentParser) -> None: add_superexec_auth_secret_args(parser) +def _add_args_http_api(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--enable-http-api", + action="store_true", + default=False, + help=( + "EXPERIMENTAL: Start one FastAPI HTTP server and let its lifespan " + "start the legacy SuperLink gRPC APIs." + ), + ) + parser.add_argument( + "--host", + default=UVICORN_DEFAULT_HOST, + help=( + "Host for the experimental FastAPI HTTP server. " + f"By default, it is set to {UVICORN_DEFAULT_HOST}." + ), + ) + parser.add_argument( + "--port", + type=_port_int, + default=UVICORN_DEFAULT_PORT, + help=( + "Port for the experimental FastAPI HTTP server. " + f"By default, it is set to {UVICORN_DEFAULT_PORT}." + ), + ) + + def _add_args_serverappio_api(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--serverappio-api-address", @@ -972,6 +1082,13 @@ def _positive_int(value: str) -> int: return parsed +def _port_int(value: str) -> int: + parsed = int(value) + if parsed < 0 or parsed > 65535: + raise argparse.ArgumentTypeError("value must be between 0 and 65535") + return parsed + + def _add_args_fleet_api(parser: argparse.ArgumentParser) -> None: # Fleet API transport layer type parser.add_argument( From ad5dda01358673d630289910998b3fbec2f80d67 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 12:56:40 +0200 Subject: [PATCH 14/33] Minor edits --- framework/py/flwr/superlink/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/py/flwr/superlink/main.py b/framework/py/flwr/superlink/main.py index 0584dc66eb8c..caf01390e382 100644 --- a/framework/py/flwr/superlink/main.py +++ b/framework/py/flwr/superlink/main.py @@ -38,8 +38,8 @@ def create_app( """Create the SuperLink FastAPI app. This FastAPI app can be started in two ways: - 1. Via `flower-superlink`: superlink_lifespan will be passed - 2. Via `uvicorn flwr.superlink.main:app`: superlink_lifespan will be None + 1. Via `flower-superlink`: `superlink_lifespan` will be passed. + 2. Via `uvicorn flwr.superlink.main:app`: `superlink_lifespan` will be None. """ @asynccontextmanager @@ -63,7 +63,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: if superlink_lifespan is not None and start_legacy_grpc: superlink_lifespan.shutdown() - log(INFO, "FastAPI lifespan: shutdown") + log(INFO, "FastAPI lifespan: shutdown") app = FastAPI( title="SuperLink API", From 498527950633d21ea78f14aba1f4c25c0f10f1a2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 14:18:56 +0200 Subject: [PATCH 15/33] Add wait_until_background_thread_exits --- framework/py/flwr/superlink/cli/flower_superlink.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index 25e0be8cc684..13ab3669d946 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -236,6 +236,7 @@ class SuperLinkLifespan: def __init__(self, config: SuperLinkLifespanConfig) -> None: self.config = config + self.bckg_threads: list[threading.Thread] = [] def startup(self) -> None: """Start shared lifespan and legacy SuperLink gRPC servers.""" @@ -245,6 +246,16 @@ def shutdown(self) -> None: """Stop legacy gRPC servers started by this lifespan.""" log(INFO, "SuperLinkLifespan: stop") + def wait_until_background_thread_exits(self) -> None: + """Block like the historical `flower-superlink` command. + + With only gRPC servers, `self.bckg_threads` is empty and `all([])` is + intentionally true, so this loop blocks until a signal handler exits the + process. This preserves the current CLI behavior. + """ + while all(thread.is_alive() for thread in self.bckg_threads): + sleep(0.1) + # pylint: disable=too-many-branches, too-many-locals, too-many-statements def flower_superlink() -> None: From 383d10bd5598588a095aebfbd9b4bdb314d64061 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 14:44:16 +0200 Subject: [PATCH 16/33] Add startup implementation --- .../py/flwr/superlink/cli/flower_superlink.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index 13ab3669d946..4afe4c8ed222 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -223,6 +223,7 @@ class SuperLinkLifespanConfig: fleet_api_type: str fleet_api_address: str | None fleet_api_num_workers: int + simulation: bool class SuperLinkLifespan: @@ -237,11 +238,29 @@ class SuperLinkLifespan: def __init__(self, config: SuperLinkLifespanConfig) -> None: self.config = config self.bckg_threads: list[threading.Thread] = [] + self.objectstore_factory: ObjectStoreFactory | None = None + self.state_factory: LinkStateFactory | None = None + self._started = False def startup(self) -> None: """Start shared lifespan and legacy SuperLink gRPC servers.""" log(INFO, "SuperLinkLifespan: start") + federation_manager = get_federation_manager(is_simulation=self.config.simulation) + objectstore_factory, state_factory = _get_objectstore_linkstate_factories( + self.config.database, federation_manager + ) + state_factory.state() # Force initialization before starting network servers + self.objectstore_factory = objectstore_factory + self.state_factory = state_factory + + self._start_control_api() + self._start_serverappio_api() + self._start_fleet_api() + self._start_superexec_if_needed() + self._start_health_server_if_needed() + self._started = True + def shutdown(self) -> None: """Stop legacy gRPC servers started by this lifespan.""" log(INFO, "SuperLinkLifespan: stop") @@ -255,6 +274,36 @@ def wait_until_background_thread_exits(self) -> None: """ while all(thread.is_alive() for thread in self.bckg_threads): sleep(0.1) + + def _start_control_api(self) -> None: + pass + + def _start_serverappio_api(self) -> None: + pass + + def _start_fleet_api(self) -> None: + pass + + def _start_legacy_fleet_rest_api( + self, host: str, port: int, num_workers: int + ) -> None: + """Start the old Fleet REST API compatibility server. + + TODO: Replace this separate uvicorn server with `flwr.superlink.routers.fleet` + routes mounted in the main FastAPI app. + """ + + def _start_legacy_fleet_grpc_rere(self, fleet_address: str) -> None: + pass + + def _start_legacy_fleet_grpc_adapter(self, fleet_address: str) -> None: + """Start the current Fleet GrpcAdapter compatibility API.""" + + def _start_superexec_if_needed(self) -> None: + pass + + def _start_health_server_if_needed(self) -> None: + pass # pylint: disable=too-many-branches, too-many-locals, too-many-statements @@ -441,6 +490,7 @@ def flower_superlink() -> None: fleet_api_type=args.fleet_api_type, fleet_api_address=args.fleet_api_address, fleet_api_num_workers=args.fleet_api_num_workers, + simulation=args.simulation, ) ########################################################################### From cedfc0563e92c2a36711e8fc86440b207da4fb74 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 14:53:10 +0200 Subject: [PATCH 17/33] Format --- .../py/flwr/superlink/cli/flower_superlink.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index 4afe4c8ed222..91ba43d8293d 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -246,7 +246,9 @@ def startup(self) -> None: """Start shared lifespan and legacy SuperLink gRPC servers.""" log(INFO, "SuperLinkLifespan: start") - federation_manager = get_federation_manager(is_simulation=self.config.simulation) + federation_manager = get_federation_manager( + is_simulation=self.config.simulation + ) objectstore_factory, state_factory = _get_objectstore_linkstate_factories( self.config.database, federation_manager ) @@ -274,15 +276,15 @@ def wait_until_background_thread_exits(self) -> None: """ while all(thread.is_alive() for thread in self.bckg_threads): sleep(0.1) - + def _start_control_api(self) -> None: - pass + config = self.config def _start_serverappio_api(self) -> None: - pass + config = self.config def _start_fleet_api(self) -> None: - pass + config = self.config def _start_legacy_fleet_rest_api( self, host: str, port: int, num_workers: int @@ -294,16 +296,17 @@ def _start_legacy_fleet_rest_api( """ def _start_legacy_fleet_grpc_rere(self, fleet_address: str) -> None: - pass + """Start the current Fleet gRPC request-response API.""" def _start_legacy_fleet_grpc_adapter(self, fleet_address: str) -> None: """Start the current Fleet GrpcAdapter compatibility API.""" def _start_superexec_if_needed(self) -> None: - pass + config = self.config def _start_health_server_if_needed(self) -> None: - pass + if self.config.health_server_address is None: + return # pylint: disable=too-many-branches, too-many-locals, too-many-statements From 5ad688154d7fc3117ced5b31e3ad5c2cedaa8561 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 15:24:25 +0200 Subject: [PATCH 18/33] Complete SuperLinkLifespan --- .../py/flwr/superlink/cli/flower_superlink.py | 185 ++++++++++++++++-- 1 file changed, 169 insertions(+), 16 deletions(-) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index 91ba43d8293d..3fdb34b03ef9 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -220,10 +220,25 @@ class SuperLinkLifespanConfig: serverappio_address: str control_address: str health_server_address: str | None + certificates: tuple[bytes, bytes, bytes] | None + appio_certificates: tuple[bytes, bytes, bytes] | None + superexec_auth_secret: bytes | None + authn_plugin: ControlAuthnPlugin + authz_plugin: ControlAuthzPlugin + event_log_plugin: EventLogWriterPlugin | None + enable_event_log: bool + artifact_provider: ArtifactProvider | None + enable_supernode_auth: bool fleet_api_type: str fleet_api_address: str | None fleet_api_num_workers: int simulation: bool + ssl_keyfile: str | None + ssl_certfile: str | None + database: str + isolation: str + appio_ssl_ca_certfile: str | None + runtime_dependency_install: bool class SuperLinkLifespan: @@ -237,7 +252,9 @@ class SuperLinkLifespan: def __init__(self, config: SuperLinkLifespanConfig) -> None: self.config = config + self.grpc_servers: list[grpc.Server] = [] self.bckg_threads: list[threading.Thread] = [] + self.superexec_process: subprocess.Popen[bytes] | None = None self.objectstore_factory: ObjectStoreFactory | None = None self.state_factory: LinkStateFactory | None = None self._started = False @@ -245,6 +262,8 @@ def __init__(self, config: SuperLinkLifespanConfig) -> None: def startup(self) -> None: """Start shared lifespan and legacy SuperLink gRPC servers.""" log(INFO, "SuperLinkLifespan: start") + if self._started: + return federation_manager = get_federation_manager( is_simulation=self.config.simulation @@ -266,6 +285,9 @@ def startup(self) -> None: def shutdown(self) -> None: """Stop legacy gRPC servers started by this lifespan.""" log(INFO, "SuperLinkLifespan: stop") + if not self._started: + return + def wait_until_background_thread_exits(self) -> None: """Block like the historical `flower-superlink` command. @@ -279,12 +301,74 @@ def wait_until_background_thread_exits(self) -> None: def _start_control_api(self) -> None: config = self.config + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + control_server: grpc.Server = run_control_api_grpc( + address=config.control_address, + state_factory=self.state_factory, + objectstore_factory=self.objectstore_factory, + certificates=config.certificates, + authn_plugin=config.authn_plugin, + authz_plugin=config.authz_plugin, + event_log_plugin=config.event_log_plugin, + artifact_provider=config.artifact_provider, + fleet_api_type=config.fleet_api_type, + ) + self.grpc_servers.append(control_server) def _start_serverappio_api(self) -> None: config = self.config + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + serverappio_server: grpc.Server = run_serverappio_api_grpc( + address=config.serverappio_address, + state_factory=self.state_factory, + objectstore_factory=self.objectstore_factory, + certificates=config.appio_certificates, + superexec_auth_secret=config.superexec_auth_secret, + ) + self.grpc_servers.append(serverappio_server) def _start_fleet_api(self) -> None: config = self.config + if config.simulation: + return + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + fleet_api_address = config.fleet_api_address + if not fleet_api_address: + if config.fleet_api_type in [ + TRANSPORT_TYPE_GRPC_RERE, + TRANSPORT_TYPE_GRPC_ADAPTER, + ]: + fleet_api_address = FLEET_API_GRPC_RERE_DEFAULT_ADDRESS + elif config.fleet_api_type == TRANSPORT_TYPE_REST: + fleet_api_address = FLEET_API_REST_DEFAULT_ADDRESS + + fleet_address, host, port = _format_address(cast(str, fleet_api_address)) + num_workers = config.fleet_api_num_workers + if num_workers != 1: + log( + WARN, + "The Fleet API currently supports only 1 worker. " + "You have specified %d workers. " + "Support for multiple workers will be added in future releases. " + "Proceeding with a single worker.", + config.fleet_api_num_workers, + ) + num_workers = 1 + + if config.fleet_api_type == TRANSPORT_TYPE_REST: + self._start_legacy_fleet_rest_api(host, port, num_workers) + elif config.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE: + self._start_legacy_fleet_grpc_rere(fleet_address) + elif config.fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER: + self._start_legacy_fleet_grpc_adapter(fleet_address) + else: + raise ValueError(f"Unknown fleet_api_type: {config.fleet_api_type}") def _start_legacy_fleet_rest_api( self, host: str, port: int, num_workers: int @@ -294,20 +378,90 @@ def _start_legacy_fleet_rest_api( TODO: Replace this separate uvicorn server with `flwr.superlink.routers.fleet` routes mounted in the main FastAPI app. """ + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + if ( + importlib.util.find_spec("requests") + and importlib.util.find_spec("starlette") + and importlib.util.find_spec("uvicorn") + ) is None: + flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST) + + fleet_thread = threading.Thread( + target=_run_fleet_api_rest, + args=( + host, + port, + self.config.ssl_keyfile, + self.config.ssl_certfile, + self.state_factory, + self.objectstore_factory, + num_workers, + ), + daemon=True, + ) + fleet_thread.start() + self.bckg_threads.append(fleet_thread) def _start_legacy_fleet_grpc_rere(self, fleet_address: str) -> None: """Start the current Fleet gRPC request-response API.""" + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + interceptors = [NodeAuthServerInterceptor(self.state_factory)] + if self.config.enable_event_log: + fleet_log_plugin = _try_obtain_fleet_event_log_writer_plugin() + if fleet_log_plugin is not None: + interceptors.append(FleetEventLogInterceptor(fleet_log_plugin)) + log(INFO, "Flower Fleet event logging enabled") + + fleet_server = _run_fleet_api_grpc_rere( + address=fleet_address, + state_factory=self.state_factory, + objectstore_factory=self.objectstore_factory, + enable_supernode_auth=self.config.enable_supernode_auth, + certificates=self.config.certificates, + interceptors=interceptors, + ) + self.grpc_servers.append(fleet_server) def _start_legacy_fleet_grpc_adapter(self, fleet_address: str) -> None: """Start the current Fleet GrpcAdapter compatibility API.""" + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + fleet_server = _run_fleet_api_grpc_adapter( + address=fleet_address, + state_factory=self.state_factory, + objectstore_factory=self.objectstore_factory, + certificates=self.config.certificates, + ) + self.grpc_servers.append(fleet_server) def _start_superexec_if_needed(self) -> None: config = self.config + if config.isolation != ISOLATION_MODE_SUBPROCESS: + return + + serverappio_server = self.grpc_servers[1] + appio_address = resolve_bind_address(serverappio_server.bound_address) + command = _get_superexec_command( + appio_address=appio_address, + appio_certificates=config.appio_certificates, + appio_root_certificates_path=config.appio_ssl_ca_certfile, + parent_pid=os.getpid(), + runtime_dependency_install=config.runtime_dependency_install, + ) + # pylint: disable-next=consider-using-with + self.superexec_process = subprocess.Popen(command) def _start_health_server_if_needed(self) -> None: if self.config.health_server_address is None: return + health_server = run_health_server_grpc_no_tls(self.config.health_server_address) + self.grpc_servers.append(health_server) + # pylint: disable=too-many-branches, too-many-locals, too-many-statements def flower_superlink() -> None: @@ -490,10 +644,25 @@ def flower_superlink() -> None: serverappio_address=serverappio_address, control_address=control_address, health_server_address=health_server_address, + certificates=certificates, + appio_certificates=appio_certificates, + superexec_auth_secret=superexec_auth_secret, + authn_plugin=authn_plugin, + authz_plugin=authz_plugin, + event_log_plugin=event_log_plugin, + enable_event_log=getattr(args, "enable_event_log", False), + artifact_provider=artifact_provider, + enable_supernode_auth=enable_supernode_auth, fleet_api_type=args.fleet_api_type, fleet_api_address=args.fleet_api_address, fleet_api_num_workers=args.fleet_api_num_workers, simulation=args.simulation, + ssl_keyfile=args.ssl_keyfile, + ssl_certfile=args.ssl_certfile, + database=args.database, + isolation=args.isolation, + appio_ssl_ca_certfile=args.appio_ssl_ca_certfile, + runtime_dependency_install=args.runtime_dependency_install, ) ########################################################################### @@ -523,22 +692,6 @@ def flower_superlink() -> None: state_factory.state() # Force initialization before starting servers - # Start Control API - is_simulation = args.simulation - control_server: grpc.Server = run_control_api_grpc( - address=control_address, - state_factory=state_factory, - objectstore_factory=objectstore_factory, - certificates=certificates, - authn_plugin=authn_plugin, - authz_plugin=authz_plugin, - event_log_plugin=event_log_plugin, - artifact_provider=artifact_provider, - fleet_api_type=args.fleet_api_type, - ) - grpc_servers = [control_server] - bckg_threads: list[threading.Thread] = [] - # Start ServerAppIo API for both deployment and simulation runtimes. serverappio_server: grpc.Server = run_serverappio_api_grpc( address=serverappio_address, From 2f4dd78bd05fa8a1ec908311bb3bd87352d35fd6 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 15:41:32 +0200 Subject: [PATCH 19/33] Replace legacy startup with lifespan --- framework/FASTAPI.md | 21 ++- .../py/flwr/superlink/cli/flower_superlink.py | 153 ++++-------------- 2 files changed, 53 insertions(+), 121 deletions(-) diff --git a/framework/FASTAPI.md b/framework/FASTAPI.md index aaa49b890a1f..ef0caaa2b886 100644 --- a/framework/FASTAPI.md +++ b/framework/FASTAPI.md @@ -1,10 +1,27 @@ # FastAPI +## SuperLink Operating Modes + +With the new HTTP API, there are now four different options to start and run the SuperLink: + +1. **Legacy Mode** `flower-superlink` (without `--enable-http-api`): This starts the SuperLink in "legacy mode" with only gRPC APIs, but no HTTP API. +2. **Compatibility Mode** `flower-superlink --enable-http-api`: Tthis starts the SuperLink in Compatibility Mode with both the HTTP API and the legacy gRPC APIs. **This is what we're running in prod until the gRPC-to-HTTP conversion is complete.** Note that in Compatibility Mode, FastAPI is limited to only 1 worker, which is a serious limitation during this transition. + + ``` + uv run flower-superlink --enable-http-api --insecure + ``` + +3. **Next Mode** `flower-superlink --enable-http-api --disable-grpc-api` with `--enable-http-api` and `--disable-grpc-api`: This starts the SuperLink in "HTTP mode" with only the HTTP API, but not the legacy gRPC APIs +4. **Experimental Mode** `uvicorn flwr.superlink.main:app`: This starts the SuperLink in "experimental mode" via uvicorn, skipping the `flower-superlink` argument parsing. This mode is experimental because it needs to reach parity with `flower-superlink --enable-http-api --disable-grpc-api`. + + ## Install -~~~ +To run FastAPI, install `flwr` with all extras to ensure the `rest` extra is included: + +``` uv sync --locked --all-extras -~~~ +``` ## Run diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index 3fdb34b03ef9..bb37cdb064c6 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -288,6 +288,36 @@ def shutdown(self) -> None: if not self._started: return + # Stop in reverse startup order so dependent services disappear before + # their backing state is considered unavailable. + for grpc_server in reversed(self.grpc_servers): + grpc_server.stop(grace=1) + + # The old REST Fleet server uses `uvicorn.run` in a daemon thread and + # does not expose a clean stop handle. Keep the join bounded so FastAPI + # shutdown cannot hang forever while this temporary compatibility path + # still exists. + for thread in self.bckg_threads: + thread.join(timeout=1.0) + if thread.is_alive(): + log( + WARN, + "Background thread %s is still running during SuperLink " + "runtime shutdown.", + thread.name, + ) + + if self.superexec_process is not None: + self.superexec_process.terminate() + try: + self.superexec_process.wait(timeout=1.0) + except subprocess.TimeoutExpired: + log(WARN, "SuperExec subprocess did not terminate within 1 second.") + + self.grpc_servers.clear() + self.bckg_threads.clear() + self.superexec_process = None + self._started = False def wait_until_background_thread_exits(self) -> None: """Block like the historical `flower-superlink` command. @@ -679,136 +709,21 @@ def flower_superlink() -> None: # Run SuperLink in Legacy Mode (Only gRPC) ########################################################################### - # Load Federation Manager - federation_manager = get_federation_manager(is_simulation=args.simulation) - - # Initialize backend ObjectStoreFactory and StateFactory + superlink_lifespan = SuperLinkLifespan(lifespan_config) try: - objectstore_factory, state_factory = _get_objectstore_linkstate_factories( - args.database, federation_manager - ) + superlink_lifespan.startup() except ValueError as err: flwr_exit(ExitCode.SUPERLINK_INVALID_ARGS, str(err)) - state_factory.state() # Force initialization before starting servers - - # Start ServerAppIo API for both deployment and simulation runtimes. - serverappio_server: grpc.Server = run_serverappio_api_grpc( - address=serverappio_address, - state_factory=state_factory, - objectstore_factory=objectstore_factory, - certificates=appio_certificates, - superexec_auth_secret=superexec_auth_secret, - ) - grpc_servers.append(serverappio_server) - - # Start Fleet API - if not is_simulation: - if not args.fleet_api_address: - if args.fleet_api_type in [ - TRANSPORT_TYPE_GRPC_RERE, - TRANSPORT_TYPE_GRPC_ADAPTER, - ]: - args.fleet_api_address = FLEET_API_GRPC_RERE_DEFAULT_ADDRESS - elif args.fleet_api_type == TRANSPORT_TYPE_REST: - args.fleet_api_address = FLEET_API_REST_DEFAULT_ADDRESS - - fleet_address, host, port = _format_address(args.fleet_api_address) - - num_workers = args.fleet_api_num_workers - if num_workers != 1: - log( - WARN, - "The Fleet API currently supports only 1 worker. " - "You have specified %d workers. " - "Support for multiple workers will be added in future releases. " - "Proceeding with a single worker.", - args.fleet_api_num_workers, - ) - num_workers = 1 - - if args.fleet_api_type == TRANSPORT_TYPE_REST: - if ( - importlib.util.find_spec("requests") - and importlib.util.find_spec("starlette") - and importlib.util.find_spec("uvicorn") - ) is None: - flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST) - - fleet_thread = threading.Thread( - target=_run_fleet_api_rest, - args=( - host, - port, - args.ssl_keyfile, - args.ssl_certfile, - state_factory, - objectstore_factory, - num_workers, - ), - daemon=True, - ) - fleet_thread.start() - bckg_threads.append(fleet_thread) - elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE: - - interceptors = [NodeAuthServerInterceptor(state_factory)] - if getattr(args, "enable_event_log", None): - fleet_log_plugin = _try_obtain_fleet_event_log_writer_plugin() - if fleet_log_plugin is not None: - interceptors.append(FleetEventLogInterceptor(fleet_log_plugin)) - log(INFO, "Flower Fleet event logging enabled") - - fleet_server = _run_fleet_api_grpc_rere( - address=fleet_address, - state_factory=state_factory, - objectstore_factory=objectstore_factory, - enable_supernode_auth=enable_supernode_auth, - certificates=certificates, - interceptors=interceptors, - ) - grpc_servers.append(fleet_server) - elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER: - fleet_server = _run_fleet_api_grpc_adapter( - address=fleet_address, - state_factory=state_factory, - objectstore_factory=objectstore_factory, - certificates=certificates, - ) - grpc_servers.append(fleet_server) - else: - raise ValueError(f"Unknown fleet_api_type: {args.fleet_api_type}") - - # Launch SuperExec if isolation mode is subprocess - if args.isolation == ISOLATION_MODE_SUBPROCESS: - # bound_address contains the actual address when the port is set to :0 - # which means let the OS choose a free port. - appio_address = resolve_bind_address(serverappio_server.bound_address) - command = _get_superexec_command( - appio_address=appio_address, - appio_certificates=appio_certificates, - appio_root_certificates_path=args.appio_ssl_ca_certfile, - parent_pid=os.getpid(), - runtime_dependency_install=args.runtime_dependency_install, - ) - # pylint: disable-next=consider-using-with - subprocess.Popen(command) - - # Launch gRPC health server - if health_server_address is not None: - health_server = run_health_server_grpc_no_tls(health_server_address) - grpc_servers.append(health_server) - # Graceful shutdown register_signal_handlers( event_type=EventType.RUN_SUPERLINK_LEAVE, exit_message="SuperLink terminated gracefully.", - grpc_servers=grpc_servers, + grpc_servers=superlink_lifespan.grpc_servers, ) # Block until a thread exits prematurely - while all(thread.is_alive() for thread in bckg_threads): - sleep(0.1) + superlink_lifespan.wait_until_background_thread_exits() # Exit if any thread has exited prematurely # This code will not be reached if the SuperLink stops gracefully From 747c38b027299b823898ca823efed51d3251cf1c Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 16:42:55 +0200 Subject: [PATCH 20/33] Add --disable-grpc-api --- .../py/flwr/superlink/cli/flower_superlink.py | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index bb37cdb064c6..4c08a372fb7c 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -507,6 +507,8 @@ def flower_superlink() -> None: backup_count=args.log_rotation_backup_count, ) + _validate_http_api_args(args) + log(INFO, "Starting Flower SuperLink") event(EventType.RUN_SUPERLINK_ENTER) @@ -751,7 +753,9 @@ def _run_superlink_http_api( REST routers should call shared SuperLink services directly and this runtime should no longer bind gRPC ports. """ - if lifespan_config.fleet_api_type == TRANSPORT_TYPE_REST: + start_legacy_grpc = not args.disable_grpc_api + + if start_legacy_grpc and lifespan_config.fleet_api_type == TRANSPORT_TYPE_REST: flwr_exit( ExitCode.SUPERLINK_INVALID_ARGS, "`--enable-http-api` cannot be combined with `--fleet-api-type rest`", @@ -766,19 +770,30 @@ def _run_superlink_http_api( except ModuleNotFoundError: flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST) - superlink_lifespan = SuperLinkLifespan(lifespan_config) + superlink_lifespan = ( + SuperLinkLifespan(lifespan_config) if start_legacy_grpc else None + ) fastapi_app = create_app( superlink_lifespan=superlink_lifespan, - start_legacy_grpc=True, + start_legacy_grpc=start_legacy_grpc, ) - log( - WARN, - "EXPERIMENTAL: Starting the combined SuperLink FastAPI service on %s:%s. " - "The legacy gRPC APIs are started from FastAPI lifespan.", - args.host, - args.port, - ) + if start_legacy_grpc: + log( + WARN, + "EXPERIMENTAL: Starting the combined SuperLink FastAPI service on %s:%s. " + "The legacy gRPC APIs are started from FastAPI lifespan.", + args.host, + args.port, + ) + else: + log( + WARN, + "EXPERIMENTAL: Starting the SuperLink FastAPI service on %s:%s. " + "The legacy gRPC APIs are disabled.", + args.host, + args.port, + ) # Uvicorn workers must stay at 1 while the lifespan starts gRPC servers. With # multiple workers, every worker process would try to bind the same Control, @@ -795,6 +810,16 @@ def _run_superlink_http_api( ) +def _validate_http_api_args(args: argparse.Namespace) -> None: + """Validate relationships between experimental HTTP API CLI flags.""" + if args.disable_grpc_api and not args.enable_http_api: + flwr_exit( + ExitCode.SUPERLINK_INVALID_ARGS, + "`--disable-grpc-api` can only be used together with " + "`--enable-http-api`.", + ) + + def _obtain_superlink_certificates( args: argparse.Namespace, ) -> tuple[tuple[bytes, bytes, bytes] | None, tuple[bytes, bytes, bytes] | None]: @@ -1155,6 +1180,15 @@ def _add_args_http_api(parser: argparse.ArgumentParser) -> None: "start the legacy SuperLink gRPC APIs." ), ) + parser.add_argument( + "--disable-grpc-api", + action="store_true", + default=False, + help=( + "EXPERIMENTAL: When used with `--enable-http-api`, start only the " + "HTTP API and do not start the legacy SuperLink gRPC APIs." + ), + ) parser.add_argument( "--host", default=UVICORN_DEFAULT_HOST, From 56dc40990ca5231cabdd3a56084d2781eaf4ea1b Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Wed, 24 Jun 2026 16:46:24 +0200 Subject: [PATCH 21/33] Update FASTAPI.md --- framework/FASTAPI.md | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/framework/FASTAPI.md b/framework/FASTAPI.md index ef0caaa2b886..7a1e5ee18e66 100644 --- a/framework/FASTAPI.md +++ b/framework/FASTAPI.md @@ -1,29 +1,42 @@ # FastAPI -## SuperLink Operating Modes +## Install + +To run FastAPI, install `flwr` with all extras to ensure the `rest` extra is included: + +``` +uv sync --locked --all-extras +``` + +## SuperLink Run Modes With the new HTTP API, there are now four different options to start and run the SuperLink: 1. **Legacy Mode** `flower-superlink` (without `--enable-http-api`): This starts the SuperLink in "legacy mode" with only gRPC APIs, but no HTTP API. + + ``` + uv run flower-superlink --insecure + ``` + 2. **Compatibility Mode** `flower-superlink --enable-http-api`: Tthis starts the SuperLink in Compatibility Mode with both the HTTP API and the legacy gRPC APIs. **This is what we're running in prod until the gRPC-to-HTTP conversion is complete.** Note that in Compatibility Mode, FastAPI is limited to only 1 worker, which is a serious limitation during this transition. ``` - uv run flower-superlink --enable-http-api --insecure + uv run flower-superlink --insecure --enable-http-api ``` 3. **Next Mode** `flower-superlink --enable-http-api --disable-grpc-api` with `--enable-http-api` and `--disable-grpc-api`: This starts the SuperLink in "HTTP mode" with only the HTTP API, but not the legacy gRPC APIs -4. **Experimental Mode** `uvicorn flwr.superlink.main:app`: This starts the SuperLink in "experimental mode" via uvicorn, skipping the `flower-superlink` argument parsing. This mode is experimental because it needs to reach parity with `flower-superlink --enable-http-api --disable-grpc-api`. + ``` + uv run flower-superlink --insecure --enable-http-api --disable-grpc-api + ``` -## Install - -To run FastAPI, install `flwr` with all extras to ensure the `rest` extra is included: +4. **Experimental Mode** `uvicorn flwr.superlink.main:app`: This starts the SuperLink in "experimental mode" via uvicorn, skipping the `flower-superlink` argument parsing. This mode is experimental because it needs to reach parity with `flower-superlink --enable-http-api --disable-grpc-api`. -``` -uv sync --locked --all-extras -``` + ``` + uv run uvicorn flwr.superlink.main:app + ``` -## Run +## Run SuperLink in Experimental Mode Start the SuperLink's FastAPI server using uvicorn: @@ -31,6 +44,8 @@ Start the SuperLink's FastAPI server using uvicorn: uv run uvicorn flwr.superlink.main:app ``` +## Run SuperNode in Experimental Mode + Start the SuperNode's FastAPI server using uvicorn: ``` From 6529abc974fe2dad7d62bbf1bc15d63e3de48776 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 25 Jun 2026 14:23:39 +0200 Subject: [PATCH 22/33] Depend on fastapi without the standard extra --- framework/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/pyproject.toml b/framework/pyproject.toml index 19eeb6a32174..656d5131ad14 100644 --- a/framework/pyproject.toml +++ b/framework/pyproject.toml @@ -71,7 +71,7 @@ simulation = [ rest = [ "starlette>=1.3.1,<1.4.0", "uvicorn[standard]>=0.49.0,<0.50.0", - "fastapi[standard]>=0.138.0,<0.139.0", + "fastapi>=0.138.0,<0.139.0", ] agent = ["browser-use[core]>=0.13.1,<0.14.0", "trafilatura>=2.1.0,<3.0.0"] From 0451914ede798255090961836c1f0450486caea7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 25 Jun 2026 14:28:34 +0200 Subject: [PATCH 23/33] Update uv.lock --- framework/uv.lock | 345 ++-------------------------------------------- 1 file changed, 10 insertions(+), 335 deletions(-) diff --git a/framework/uv.lock b/framework/uv.lock index 92288b49a184..9f07e492fca7 100644 --- a/framework/uv.lock +++ b/framework/uv.lock @@ -1144,15 +1144,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] -[[package]] -name = "detect-installer" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, -] - [[package]] name = "devtool" version = "0.0.0" @@ -1234,15 +1225,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] -[[package]] -name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - [[package]] name = "docopt" version = "0.6.2" @@ -1322,19 +1304,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", size = 16360, upload-time = "2025-06-09T08:21:35.654Z" }, ] -[[package]] -name = "email-validator" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, -] - [[package]] name = "executing" version = "2.2.1" @@ -1360,160 +1329,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/ff/8496d9847a5fedae775eb49460722d3efaa80487854273e9647ae876218c/fastapi-0.138.0-py3-none-any.whl", hash = "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", size = 126779, upload-time = "2026-06-20T01:18:03.483Z" }, ] -[package.optional-dependencies] -standard = [ - { name = "email-validator" }, - { name = "fastapi-cli", extra = ["standard"] }, - { name = "fastar" }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "pydantic-extra-types" }, - { name = "pydantic-settings" }, - { name = "python-multipart" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[[package]] -name = "fastapi-cli" -version = "0.0.27" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rich-toolkit" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/37/d0/ee5678346811967b8d096d5d5604e71b50d6bf5a2abfbdb331157e2bbaa9/fastapi_cli-0.0.27.tar.gz", hash = "sha256:1dffb1e40c0c88f2e0171a8a252a2b615c1e63ff8c05626649e4badd6a84336a", size = 23630, upload-time = "2026-06-18T14:48:43.421Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/ab/0a709f9488fe62647db80f8a277fb0ee62e85adc6746abf477ed373c9eb7/fastapi_cli-0.0.27-py3-none-any.whl", hash = "sha256:2e389a40f318e29fec8cb1e289f267f17c048876fb82dbfa869a10b16740495d", size = 13070, upload-time = "2026-06-18T14:48:44.311Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "fastapi-cloud-cli" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[[package]] -name = "fastapi-cloud-cli" -version = "0.20.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "detect-installer" }, - { name = "fastar" }, - { name = "httpx" }, - { name = "pydantic", extra = ["email"] }, - { name = "rich-toolkit" }, - { name = "rignore" }, - { name = "sentry-sdk" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ad/bf/97d19633c6ec6fb0ef59df474b9705ea992f7b4f879208d0007ac6d25ab6/fastapi_cloud_cli-0.20.0.tar.gz", hash = "sha256:9681c46adcd299024d0775658bd5d88992fd35c4ad42b1f045c6df913390ba37", size = 85904, upload-time = "2026-06-11T17:41:02.814Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/6e/bbb2e1b8f3170b6426b707d49981a838fc1d5cbb428dd9a271f1c3951c23/fastapi_cloud_cli-0.20.0-py3-none-any.whl", hash = "sha256:dcbf071fc659ae2d3fb30e221a661c3fa240b7d5091203cf941face31f6d7860", size = 68793, upload-time = "2026-06-11T17:41:01.804Z" }, -] - -[[package]] -name = "fastar" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/7a/fb367bdaf4efa2c7952a45aeab2e87a564293ecffe150af673ec8edfda46/fastar-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b82fd6f996e65a86f67a6bd64dd22ef3e8ae2dcaed0ae3b550e71f7e1bbb1df5", size = 709869, upload-time = "2026-04-13T17:09:55.62Z" }, - { url = "https://files.pythonhosted.org/packages/80/ff/b87efb0dcfd081c62c7c7601d7681dabe63103cd51fc16f8d57a1ab45961/fastar-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27eed386fd0558e6daa29211111bbd7b740f7c7e881197f8a00ac7c0f3cdb1d7", size = 631668, upload-time = "2026-04-13T17:09:40.537Z" }, - { url = "https://files.pythonhosted.org/packages/24/7c/0ed6dd38b9adc04b3a8ec3b7045908e7c2170ba0ff6e6d2c51bc9fc770f3/fastar-0.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a6931bebc1d8e95ddeef55732c195449e6b44ef33aa31b325505097ed3b4d6aa", size = 869663, upload-time = "2026-04-13T17:09:09.78Z" }, - { url = "https://files.pythonhosted.org/packages/58/ce/8b7fb3f23855accebaaf2d2637eac7f261a7a5d936f861a172079f1ef511/fastar-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f72ce42a5e28a74fbd4d5fbf1a3ac1a1163d13cbc200cbd005fb0fabc54bd", size = 762938, upload-time = "2026-04-13T17:07:54.51Z" }, - { url = "https://files.pythonhosted.org/packages/07/cc/5491e2b677bb841f768e3aba052d0344338a5c78aa5d4c18b443831a8e8d/fastar-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5b83c1f61f7017d6e1498568038f8745440cfc16ca2f697ec81bac83050108f6", size = 759232, upload-time = "2026-04-13T17:08:08.864Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/643630bdbd179e41e9fae31c03b4cf6061dbf4d6fbbae8425d16eb12545d/fastar-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db73a9b765a516e73983b25341e7b5e0189733878279e278b2295131b0e3a21e", size = 926271, upload-time = "2026-04-13T17:08:23.68Z" }, - { url = "https://files.pythonhosted.org/packages/09/5d/37ade50003b4540e0a53ef100f6692d7ab2ac1122d5acf39920cc09a3e8b/fastar-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:625827d52eb4e8fec942e0233f125ff8010fcf6a67c0a974a8e5f4666b771e3c", size = 818634, upload-time = "2026-04-13T17:08:54.268Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ff/135d177de32cc1e837c99019e4643e6e79352bde49544d4ece5b5eebf56b/fastar-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5fd8fa21ec0a88296a38dc5d7fc35efd3b26d46a17b8b7c73c5563925ca15", size = 822755, upload-time = "2026-04-13T17:09:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/27/cb/b835dbe76ceac7fa6105851468c259ffd06830eb9c029402e499d0ec153b/fastar-0.11.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8c15af91b8cd87ddf23ea55355ae513c1de3ab67178f26dad017c9e9c0af6096", size = 887101, upload-time = "2026-04-13T17:08:39.248Z" }, - { url = "https://files.pythonhosted.org/packages/9e/54/aa8289eb57fc550535470397cb051f5a58a7c89ca4de31d5502b916dd894/fastar-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a112395a8b0bff251423bd1564c012f0cc058ad8b6bd8fba96f3d7fc117e44", size = 973606, upload-time = "2026-04-13T17:10:10.98Z" }, - { url = "https://files.pythonhosted.org/packages/1f/fd/776d50a0897c01dc6bfd0926772ee913436fdae91b9affaf0a0cbd09f0a1/fastar-0.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2994bb8f5f8c11eb12beae1e6e77a907173c9819236b8a4c8f0573652ceccce", size = 1036696, upload-time = "2026-04-13T17:10:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f1/cf0f9b499fb37ac065c8a01ec642f96a3c5eb849c38ae983b59f3b3245e0/fastar-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dcf99e4b5973d842c7f19c776c3a83cdc0977d505edce6206438505c0456b517", size = 1078182, upload-time = "2026-04-13T17:10:45.318Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9e/21e4701aec4a1123d4dc4d31578dc18875582b5710e4725f7ceb752a248b/fastar-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29c9c386dc0d5dda78845a8e6b1480d26ab861c1e0b68f42ae5735cb70ca07f1", size = 1032336, upload-time = "2026-04-13T17:11:02.364Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e2/5872b28c72c27ec1a00760eace6ff35f714f41ebbd5208cf016b12e29250/fastar-0.11.0-cp311-cp311-win32.whl", hash = "sha256:030b2580fc394f2c9b7890b6735810404e9b9ed5e0344db150b945965b5482b7", size = 457368, upload-time = "2026-04-13T17:11:43.528Z" }, - { url = "https://files.pythonhosted.org/packages/fd/6e/ce6832a16193eb4466f4108be8809c249b51cb1f89dd7894545700d079d5/fastar-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:83ab57ae067969cd0b483ac3b6dccc4b595fc77f5c820760998648d4c42822b5", size = 488605, upload-time = "2026-04-13T17:11:29.161Z" }, - { url = "https://files.pythonhosted.org/packages/15/5a/9cfb80661cf38fd7b0889224beb7d2746784d4ade2a931ed9775a18d8602/fastar-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:27b1a4cee2298b704de8151d310462ee7335ed036011ca9aa6e784b30b6c73a9", size = 464580, upload-time = "2026-04-13T17:11:18.583Z" }, - { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, - { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, - { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, - { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, - { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, - { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, - { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, - { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, - { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, - { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, - { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, - { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, - { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, - { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, - { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, - { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, - { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, - { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, - { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, - { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, - { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, - { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, - { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, - { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, - { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, - { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, - { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, - { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, - { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, - { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, - { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, - { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, - { url = "https://files.pythonhosted.org/packages/cc/5c/9bbeffbf1905391446dd98aa520422ce7affde5c9a7c22d757cc5d7c1397/fastar-0.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1266d6a004f427b0d61bd6c7b544d84cc964691b2232c2f4d635a1b75f2f6d5e", size = 711644, upload-time = "2026-04-13T17:10:07.663Z" }, - { url = "https://files.pythonhosted.org/packages/7e/af/ae5cf39d4fb82d0c592705f5ec6db1b065be5265c151b108f86126ee8773/fastar-0.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:298a827ec04ade43733f6ca960d0faec38706aa1494175869ea7ea17f5bad5d3", size = 634371, upload-time = "2026-04-13T17:09:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/7e/36/8d4569e26473c72ccb02d1c5df3ed710073f1c06eca09c26d52ea79fd815/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8800e2387e463a0e5799416a1cbe72dd0fde7270a20e4bde684145e7878f6516", size = 870850, upload-time = "2026-04-13T17:09:21.439Z" }, - { url = "https://files.pythonhosted.org/packages/bf/46/724dc796e1756d3977970f820d30d59bb8cab8e3671b285f1d82ab513aec/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7496def0a2befd82d429cb004ef7ca831585cc887947bd6b9abb68a5ef852b0b", size = 764469, upload-time = "2026-04-13T17:08:05.638Z" }, - { url = "https://files.pythonhosted.org/packages/99/e3/74d6859e632e8fb9339a14f652fb9f800c2bd6aa53071e311c0be3fbab8b/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:878eaf15463eb572e3538af7ca3a8534e5e279cf8196db902d24e5725c4af86e", size = 761375, upload-time = "2026-04-13T17:08:20.669Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e7/cc70e2be5ef8731a7525552b1c35c1448cf9eae6a62cb3a56f12c1bf27ea/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0324ed1d1ef0186e1bbd843b17807d6d837d0906899d4c99378b02c5d86bdd9c", size = 928189, upload-time = "2026-04-13T17:08:35.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/33/c9a969e78dca323547276a6fee5f4f9588f7cd5ab45acec3778c67399589/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdf9bd863205590beaf8ef6e66f315310196632180dceaf674985d01a876cac3", size = 820864, upload-time = "2026-04-13T17:09:06.366Z" }, - { url = "https://files.pythonhosted.org/packages/84/bd/6b9434b541fe55c125b5f2e017a565596a2d215aa09207e4555e4585064f/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59af8dbb683b24b90fb5b506de080faeab0a17a908e6c2a5d93a97260ed75d7b", size = 824060, upload-time = "2026-04-13T17:09:37.377Z" }, - { url = "https://files.pythonhosted.org/packages/24/8d/871d5f8cf4c6f13987119fb0a9ae8be131e34f2756c2524e9974adf33824/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:9f3df73a3c4292cfe15696cdf59cdb6c309ab59d30b34c733be13c6e32d9a264", size = 889217, upload-time = "2026-04-13T17:08:50.884Z" }, - { url = "https://files.pythonhosted.org/packages/d0/26/cca0fd2704f3ed20165e5613ed911549aef3aaf3b0b5b02fee0e8e23e6cc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa3762cbb16e41a76b61f4a6914937a71aab3a7b6c2d82ca233bc686ebaf756b", size = 975418, upload-time = "2026-04-13T17:10:24.307Z" }, - { url = "https://files.pythonhosted.org/packages/99/94/8bbb0b13f5b6cbe2492f0b7cbba5103e6163976a3331466d010e781fa189/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a8c7bc8ac74cb359bb546b199288c83236372d094b402e557c197e85527495cd", size = 1038492, upload-time = "2026-04-13T17:10:41.939Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d3/5b7df222a30eac2822ffd00f82fd4c2ce84fba4b369d1e1a03732fd177fc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:587cbd060a2699c5f66281081395bb4657b2b1e0eef5c206b1aabf740019d670", size = 1080210, upload-time = "2026-04-13T17:10:58.462Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/56ef943ea524784598c035ccbd42e564e937da0438ae3f55f0e76cb95571/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a1c56957ac82408be37a3f63594bc83e0919e8760492a4475e542f9f1828778", size = 1034886, upload-time = "2026-04-13T17:11:15.617Z" }, -] - [[package]] name = "fastjsonschema" version = "2.21.2" @@ -1586,7 +1401,7 @@ agent = [ { name = "trafilatura" }, ] rest = [ - { name = "fastapi", extra = ["standard"] }, + { name = "fastapi" }, { name = "starlette" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -1656,7 +1471,7 @@ requires-dist = [ { name = "browser-use", extras = ["core"], marker = "extra == 'agent'", specifier = ">=0.13.1,<0.14.0" }, { name = "click", specifier = ">=8.0.0,<9.0.0" }, { name = "cryptography", specifier = ">=46.0.7,<47.0.0" }, - { name = "fastapi", extras = ["standard"], marker = "extra == 'rest'", specifier = ">=0.138.0,<0.139.0" }, + { name = "fastapi", marker = "extra == 'rest'", specifier = ">=0.138.0,<0.139.0" }, { name = "grpcio", specifier = ">=1.70.0,<2.0.0" }, { name = "grpcio-health-checking", specifier = ">=1.70.0,<2.0.0" }, { name = "iterators", specifier = ">=0.0.2,<0.0.3" }, @@ -4479,11 +4294,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] -[package.optional-dependencies] -email = [ - { name = "email-validator" }, -] - [[package]] name = "pydantic-core" version = "2.41.5" @@ -4581,19 +4391,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] -[[package]] -name = "pydantic-extra-types" -version = "2.11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, -] - [[package]] name = "pydantic-settings" version = "2.14.2" @@ -7914,14 +7711,14 @@ name = "ray" version = "2.55.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "filelock", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "jsonschema", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "msgpack", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "packaging", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "protobuf", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "pyyaml", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "requests", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, + { name = "click", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "filelock", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "jsonschema", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "msgpack", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "packaging", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "protobuf", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "pyyaml", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "requests", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/88/7d/48ba2f49b40a34b0071ee27c0144a2573d8836094eaca213d59cef12c271/ray-2.55.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0053fd5b400f7ac56263aa1bbd3d68fb79341b08b8dc697c88782d5aca7b3ed4", size = 65835271, upload-time = "2026-04-22T20:09:34.984Z" }, @@ -8209,115 +8006,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, ] -[[package]] -name = "rich-toolkit" -version = "0.20.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/63/3e427c62f1992945c997d4ec31e2fcb37d26aadbe5aa44ae5b29f7f64d26/rich_toolkit-0.20.1.tar.gz", hash = "sha256:c7336ae281f435c785acecaedc4b71d4b663dc73d9c8079fea96372527e822a4", size = 203473, upload-time = "2026-06-05T08:56:57.679Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/88/309f07d08155da2ba1d5ceb42d270fb42fbe34a807684543e3ffc10fe713/rich_toolkit-0.20.1-py3-none-any.whl", hash = "sha256:2a6d5f8e15759b9eba5a9ee63da10b275359ead20e5a0fc92bd5b4dbae8ce4bf", size = 35525, upload-time = "2026-06-05T08:56:58.586Z" }, -] - -[[package]] -name = "rignore" -version = "0.7.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, - { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, - { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, - { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, - { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, - { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, - { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, - { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, - { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, - { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, - { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, - { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, - { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, - { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, - { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, - { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, - { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, - { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, - { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, - { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, - { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, - { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, - { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, - { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, - { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, - { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, - { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, - { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, - { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, - { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, - { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, - { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, - { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, - { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, - { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, - { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, - { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, - { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, - { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, - { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, - { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, - { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, - { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, - { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, - { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, - { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, - { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, - { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, - { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, - { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, - { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, -] - [[package]] name = "roman" version = "5.2" @@ -8538,19 +8226,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] -[[package]] -name = "sentry-sdk" -version = "2.63.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/c8/b3c970a5b186722d276cd40a05b3254e03bccc0208560aff20f612e018e8/sentry_sdk-2.63.0.tar.gz", hash = "sha256:2a1502bf864769275dbc8c2c9fc7a0f7f5e18358180b615d262d13a31ffba216", size = 912449, upload-time = "2026-06-16T12:45:57.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/57/cb205f7d93373120f666b9c5736dc0815524d96a9b278e7a728f018dc22a/sentry_sdk-2.63.0-py3-none-any.whl", hash = "sha256:3a9b5ddd403f79eb73bd670f75f04485819db53d28f76ced7bc09041cb0dfd6a", size = 495950, upload-time = "2026-06-16T12:45:55.819Z" }, -] - [[package]] name = "setuptools" version = "82.0.0" From 76d1168acc37e7e09bd9396949ffd647bcc7e868 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 25 Jun 2026 21:18:35 +0200 Subject: [PATCH 24/33] feat(framework): Create FastAPI scaffold --- framework/FASTAPI.md | 31 +++++ .../py/flwr/supercore/routers/__init__.py | 15 ++ .../flwr/supercore/routers/health/__init__.py | 20 +++ .../flwr/supercore/routers/health/router.py | 32 +++++ framework/py/flwr/superlink/main.py | 58 ++++++++ .../py/flwr/superlink/routers/__init__.py | 15 ++ .../superlink/routers/runtime/__init__.py | 20 +++ .../flwr/superlink/routers/runtime/router.py | 29 ++++ framework/py/flwr/supernode/main.py | 58 ++++++++ .../py/flwr/supernode/routers/__init__.py | 15 ++ .../supernode/routers/runtime/__init__.py | 20 +++ .../flwr/supernode/routers/runtime/router.py | 29 ++++ framework/pyproject.toml | 6 +- framework/uv.lock | 130 +++++++++++------- 14 files changed, 429 insertions(+), 49 deletions(-) create mode 100644 framework/FASTAPI.md create mode 100644 framework/py/flwr/supercore/routers/__init__.py create mode 100644 framework/py/flwr/supercore/routers/health/__init__.py create mode 100644 framework/py/flwr/supercore/routers/health/router.py create mode 100644 framework/py/flwr/superlink/main.py create mode 100644 framework/py/flwr/superlink/routers/__init__.py create mode 100644 framework/py/flwr/superlink/routers/runtime/__init__.py create mode 100644 framework/py/flwr/superlink/routers/runtime/router.py create mode 100644 framework/py/flwr/supernode/main.py create mode 100644 framework/py/flwr/supernode/routers/__init__.py create mode 100644 framework/py/flwr/supernode/routers/runtime/__init__.py create mode 100644 framework/py/flwr/supernode/routers/runtime/router.py diff --git a/framework/FASTAPI.md b/framework/FASTAPI.md new file mode 100644 index 000000000000..0415545f8722 --- /dev/null +++ b/framework/FASTAPI.md @@ -0,0 +1,31 @@ +# FastAPI + +## Install + +```bash +uv sync --all-extras +``` + +## Run SuperLink + +Start the SuperLink FastAPI server using uvicorn: + +```bash +uv run uvicorn flwr.superlink.main:app +``` + +## Run SuperNode + +Start the SuperNode FastAPI server using uvicorn: + +```bash +uv run uvicorn flwr.supernode.main:app +``` + +## Docs + +Docs are available once the SuperLink or SuperNode FastAPI server is running: + +```text +http://127.0.0.1:8000/docs +``` diff --git a/framework/py/flwr/supercore/routers/__init__.py b/framework/py/flwr/supercore/routers/__init__.py new file mode 100644 index 000000000000..20ef5c6c6913 --- /dev/null +++ b/framework/py/flwr/supercore/routers/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperCore API routers.""" diff --git a/framework/py/flwr/supercore/routers/health/__init__.py b/framework/py/flwr/supercore/routers/health/__init__.py new file mode 100644 index 000000000000..bba2f85d8fd8 --- /dev/null +++ b/framework/py/flwr/supercore/routers/health/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Health API router.""" + + +from .router import router + +__all__ = ["router"] diff --git a/framework/py/flwr/supercore/routers/health/router.py b/framework/py/flwr/supercore/routers/health/router.py new file mode 100644 index 000000000000..3ac77bcac25a --- /dev/null +++ b/framework/py/flwr/supercore/routers/health/router.py @@ -0,0 +1,32 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Health API router implementation.""" + + +from fastapi import APIRouter, Request, Response, status + +router = APIRouter(tags=["health"]) + + +@router.api_route("/health", methods=["GET", "HEAD"]) +async def health(_: Request) -> Response: + """Report whether the API server is healthy.""" + return Response(status_code=status.HTTP_200_OK) + + +@router.api_route("/ready", methods=["GET", "HEAD"]) +async def ready(_: Request) -> Response: + """Report whether the API server is ready.""" + return Response(status_code=status.HTTP_200_OK) diff --git a/framework/py/flwr/superlink/main.py b/framework/py/flwr/superlink/main.py new file mode 100644 index 000000000000..48f1fcf3fb3b --- /dev/null +++ b/framework/py/flwr/superlink/main.py @@ -0,0 +1,58 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperLink API.""" + + +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from logging import INFO + +from fastapi import FastAPI + +from flwr import __version__ +from flwr.common import log +from flwr.supercore.routers import health +from flwr.superlink.routers import runtime + + +def create_app() -> FastAPI: + """Create the SuperLink FastAPI app.""" + + @asynccontextmanager + async def lifespan(_: FastAPI) -> AsyncIterator[None]: + log(INFO, "FastAPI lifespan: startup") + yield + log(INFO, "FastAPI lifespan: shutdown") + + app = FastAPI( + title="SuperLink API", + version=__version__, + docs_url="/docs", + redoc_url=None, + lifespan=lifespan, + ) + + # SuperCore API routers + app.include_router(health.router) + + # SuperLink API routers + app.include_router(runtime.router) + + return app + + +app = create_app() diff --git a/framework/py/flwr/superlink/routers/__init__.py b/framework/py/flwr/superlink/routers/__init__.py new file mode 100644 index 000000000000..b39cd9c1b844 --- /dev/null +++ b/framework/py/flwr/superlink/routers/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperLink API routers.""" diff --git a/framework/py/flwr/superlink/routers/runtime/__init__.py b/framework/py/flwr/superlink/routers/runtime/__init__.py new file mode 100644 index 000000000000..69272131b49e --- /dev/null +++ b/framework/py/flwr/superlink/routers/runtime/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Runtime API router.""" + + +from .router import router + +__all__ = ["router"] diff --git a/framework/py/flwr/superlink/routers/runtime/router.py b/framework/py/flwr/superlink/routers/runtime/router.py new file mode 100644 index 000000000000..76b7d30ae153 --- /dev/null +++ b/framework/py/flwr/superlink/routers/runtime/router.py @@ -0,0 +1,29 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Runtime API router.""" + + +from fastapi import APIRouter, HTTPException, Request, Response, status + +router = APIRouter(prefix="/runtime", tags=["runtime"]) + + +@router.post("/messages") +def pull_messages(_: Request) -> Response: + """Pull messages for the ServerApp.""" + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="Not implemented", + ) diff --git a/framework/py/flwr/supernode/main.py b/framework/py/flwr/supernode/main.py new file mode 100644 index 000000000000..f23584435926 --- /dev/null +++ b/framework/py/flwr/supernode/main.py @@ -0,0 +1,58 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperNode API.""" + + +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from logging import INFO + +from fastapi import FastAPI + +from flwr import __version__ +from flwr.common import log +from flwr.supercore.routers import health +from flwr.supernode.routers import runtime + + +def create_app() -> FastAPI: + """Create the SuperNode FastAPI app.""" + + @asynccontextmanager + async def lifespan(_: FastAPI) -> AsyncIterator[None]: + log(INFO, "FastAPI lifespan: startup") + yield + log(INFO, "FastAPI lifespan: shutdown") + + app = FastAPI( + title="SuperNode API", + version=__version__, + docs_url="/docs", + redoc_url=None, + lifespan=lifespan, + ) + + # SuperCore API routers + app.include_router(health.router) + + # SuperNode API routers + app.include_router(runtime.router) + + return app + + +app = create_app() diff --git a/framework/py/flwr/supernode/routers/__init__.py b/framework/py/flwr/supernode/routers/__init__.py new file mode 100644 index 000000000000..cbf90b90296e --- /dev/null +++ b/framework/py/flwr/supernode/routers/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""SuperNode API routers.""" diff --git a/framework/py/flwr/supernode/routers/runtime/__init__.py b/framework/py/flwr/supernode/routers/runtime/__init__.py new file mode 100644 index 000000000000..69272131b49e --- /dev/null +++ b/framework/py/flwr/supernode/routers/runtime/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Runtime API router.""" + + +from .router import router + +__all__ = ["router"] diff --git a/framework/py/flwr/supernode/routers/runtime/router.py b/framework/py/flwr/supernode/routers/runtime/router.py new file mode 100644 index 000000000000..387d1cd1f8ce --- /dev/null +++ b/framework/py/flwr/supernode/routers/runtime/router.py @@ -0,0 +1,29 @@ +# Copyright 2026 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Runtime API router.""" + + +from fastapi import APIRouter, HTTPException, Request, Response, status + +router = APIRouter(prefix="/runtime", tags=["runtime"]) + + +@router.post("/messages") +def pull_messages(_: Request) -> Response: + """Pull messages for the ClientApp.""" + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="Not implemented", + ) diff --git a/framework/pyproject.toml b/framework/pyproject.toml index 6ecc0b401045..656d5131ad14 100644 --- a/framework/pyproject.toml +++ b/framework/pyproject.toml @@ -68,7 +68,11 @@ simulation = [ "ray==2.55.1; python_version >= '3.11' and python_version < '3.13'", "ray==2.55.1; python_version >= '3.13' and python_version < '3.15' and sys_platform != 'win32'", ] -rest = ["starlette>=0.50.0,<0.51.0", "uvicorn[standard]>=0.40.0,<0.41.0"] +rest = [ + "starlette>=1.3.1,<1.4.0", + "uvicorn[standard]>=0.49.0,<0.50.0", + "fastapi>=0.138.0,<0.139.0", +] agent = ["browser-use[core]>=0.13.1,<0.14.0", "trafilatura>=2.1.0,<3.0.0"] [project.scripts] diff --git a/framework/uv.lock b/framework/uv.lock index 5ac62d90dc17..b50c17140691 100644 --- a/framework/uv.lock +++ b/framework/uv.lock @@ -184,6 +184,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -1304,6 +1313,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fastapi" +version = "0.138.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/c9/5e8defe249899c0dc900643695fc07829a67fc88b4ff2cdb03fcbdbf5a4b/fastapi-0.138.1.tar.gz", hash = "sha256:96e3702dce09ee0dce48856135620d3d865ca684a79fe7513fd7b13a12f82862", size = 419646, upload-time = "2026-06-25T15:40:42.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl", hash = "sha256:b994cae7ba8b82c976a728b544244de31333fa5f7d261f9a1dffe526444cae23", size = 129182, upload-time = "2026-06-25T15:40:40.771Z" }, +] + [[package]] name = "fastjsonschema" version = "2.21.2" @@ -1376,6 +1401,7 @@ agent = [ { name = "trafilatura" }, ] rest = [ + { name = "fastapi" }, { name = "starlette" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -1445,6 +1471,7 @@ requires-dist = [ { name = "browser-use", extras = ["core"], marker = "extra == 'agent'", specifier = ">=0.13.1,<0.14.0" }, { name = "click", specifier = ">=8.0.0,<9.0.0" }, { name = "cryptography", specifier = ">=46.0.7,<47.0.0" }, + { name = "fastapi", marker = "extra == 'rest'", specifier = ">=0.138.0,<0.139.0" }, { name = "grpcio", specifier = ">=1.70.0,<2.0.0" }, { name = "grpcio-health-checking", specifier = ">=1.70.0,<2.0.0" }, { name = "iterators", specifier = ">=0.0.2,<0.0.3" }, @@ -1459,13 +1486,13 @@ requires-dist = [ { name = "requests", specifier = ">=2.33.0,<3.0.0" }, { name = "rich", specifier = ">=14.0.0,<15.0.0" }, { name = "sqlalchemy", specifier = ">=2.0.45,<3.0.0" }, - { name = "starlette", marker = "extra == 'rest'", specifier = ">=0.50.0,<0.51.0" }, + { name = "starlette", marker = "extra == 'rest'", specifier = ">=1.3.1,<1.4.0" }, { name = "tomli", specifier = ">=2.0.1,<3.0.0" }, { name = "tomli-w", specifier = ">=1.0.0,<2.0.0" }, { name = "trafilatura", marker = "extra == 'agent'", specifier = ">=2.1.0,<3.0.0" }, { name = "typer", specifier = ">=0.13.0,<0.21.0" }, { name = "uv", specifier = ">=0.11.15,<0.12.0" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'rest'", specifier = ">=0.40.0,<0.41.0" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'rest'", specifier = ">=0.49.0,<0.50.0" }, ] provides-extras = ["simulation", "rest", "agent"] @@ -2008,38 +2035,45 @@ wheels = [ [[package]] name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, - { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, ] [[package]] @@ -7677,14 +7711,14 @@ name = "ray" version = "2.55.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "filelock", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "jsonschema", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "msgpack", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "packaging", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "protobuf", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "pyyaml", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, - { name = "requests", marker = "python_full_version != '3.13.*' or sys_platform != 'win32'" }, + { name = "click", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "filelock", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "jsonschema", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "msgpack", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "packaging", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "protobuf", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "pyyaml", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, + { name = "requests", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'x86_64') or (python_full_version != '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'x86_64') or sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/88/7d/48ba2f49b40a34b0071ee27c0144a2573d8836094eaca213d59cef12c271/ray-2.55.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0053fd5b400f7ac56263aa1bbd3d68fb79341b08b8dc697c88782d5aca7b3ed4", size = 65835271, upload-time = "2026-04-22T20:09:34.984Z" }, @@ -8553,15 +8587,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.50.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] @@ -8987,15 +9021,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, ] [package.optional-dependencies] From dfaf1b393f08e6ce0b9959b2a9efd019d8868671 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 25 Jun 2026 22:03:06 +0200 Subject: [PATCH 25/33] Fix tests --- .../py/flwr/server/superlink/fleet/rest_rere/rest_api.py | 6 +++--- framework/py/flwr/supercore/routers/health/router.py | 5 +++-- framework/py/flwr/superlink/main.py | 8 ++++---- framework/py/flwr/superlink/routers/runtime/router.py | 3 ++- framework/py/flwr/supernode/main.py | 8 ++++---- framework/py/flwr/supernode/routers/runtime/router.py | 3 ++- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/framework/py/flwr/server/superlink/fleet/rest_rere/rest_api.py b/framework/py/flwr/server/superlink/fleet/rest_rere/rest_api.py index c59bd5cb799a..044b18c2681e 100644 --- a/framework/py/flwr/server/superlink/fleet/rest_rere/rest_api.py +++ b/framework/py/flwr/server/superlink/fleet/rest_rere/rest_api.py @@ -57,7 +57,7 @@ try: from starlette.applications import Starlette - from starlette.datastructures import Headers + from starlette.datastructures import Headers, State from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import Response @@ -70,7 +70,7 @@ GrpcResponse = TypeVar("GrpcResponse", bound=GrpcMessage) GrpcAsyncFunction = Callable[[GrpcRequest], Awaitable[GrpcResponse]] -RestEndPoint = Callable[[Request], Awaitable[Response]] +RestEndPoint = Callable[[Request[State]], Awaitable[Response]] routes = [] @@ -82,7 +82,7 @@ def rest_request_response( def decorator(func: GrpcAsyncFunction[GrpcRequest, GrpcResponse]) -> RestEndPoint: - async def wrapper(request: Request) -> Response: + async def wrapper(request: Request[State]) -> Response: _check_headers(request.headers) # Get the request body as raw bytes diff --git a/framework/py/flwr/supercore/routers/health/router.py b/framework/py/flwr/supercore/routers/health/router.py index 3ac77bcac25a..eb5afdb6d29e 100644 --- a/framework/py/flwr/supercore/routers/health/router.py +++ b/framework/py/flwr/supercore/routers/health/router.py @@ -16,17 +16,18 @@ from fastapi import APIRouter, Request, Response, status +from starlette.datastructures import State router = APIRouter(tags=["health"]) @router.api_route("/health", methods=["GET", "HEAD"]) -async def health(_: Request) -> Response: +async def health(_: Request[State]) -> Response: """Report whether the API server is healthy.""" return Response(status_code=status.HTTP_200_OK) @router.api_route("/ready", methods=["GET", "HEAD"]) -async def ready(_: Request) -> Response: +async def ready(_: Request[State]) -> Response: """Report whether the API server is ready.""" return Response(status_code=status.HTTP_200_OK) diff --git a/framework/py/flwr/superlink/main.py b/framework/py/flwr/superlink/main.py index 48f1fcf3fb3b..08d050eaf0be 100644 --- a/framework/py/flwr/superlink/main.py +++ b/framework/py/flwr/superlink/main.py @@ -38,7 +38,7 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: yield log(INFO, "FastAPI lifespan: shutdown") - app = FastAPI( + fastapi_app = FastAPI( title="SuperLink API", version=__version__, docs_url="/docs", @@ -47,12 +47,12 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: ) # SuperCore API routers - app.include_router(health.router) + fastapi_app.include_router(health.router) # SuperLink API routers - app.include_router(runtime.router) + fastapi_app.include_router(runtime.router) - return app + return fastapi_app app = create_app() diff --git a/framework/py/flwr/superlink/routers/runtime/router.py b/framework/py/flwr/superlink/routers/runtime/router.py index 76b7d30ae153..c3036c417716 100644 --- a/framework/py/flwr/superlink/routers/runtime/router.py +++ b/framework/py/flwr/superlink/routers/runtime/router.py @@ -16,12 +16,13 @@ from fastapi import APIRouter, HTTPException, Request, Response, status +from starlette.datastructures import State router = APIRouter(prefix="/runtime", tags=["runtime"]) @router.post("/messages") -def pull_messages(_: Request) -> Response: +def pull_messages(_: Request[State]) -> Response: """Pull messages for the ServerApp.""" raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, diff --git a/framework/py/flwr/supernode/main.py b/framework/py/flwr/supernode/main.py index f23584435926..de66d5b3ce26 100644 --- a/framework/py/flwr/supernode/main.py +++ b/framework/py/flwr/supernode/main.py @@ -38,7 +38,7 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: yield log(INFO, "FastAPI lifespan: shutdown") - app = FastAPI( + fastapi_app = FastAPI( title="SuperNode API", version=__version__, docs_url="/docs", @@ -47,12 +47,12 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: ) # SuperCore API routers - app.include_router(health.router) + fastapi_app.include_router(health.router) # SuperNode API routers - app.include_router(runtime.router) + fastapi_app.include_router(runtime.router) - return app + return fastapi_app app = create_app() diff --git a/framework/py/flwr/supernode/routers/runtime/router.py b/framework/py/flwr/supernode/routers/runtime/router.py index 387d1cd1f8ce..aeefa4144303 100644 --- a/framework/py/flwr/supernode/routers/runtime/router.py +++ b/framework/py/flwr/supernode/routers/runtime/router.py @@ -16,12 +16,13 @@ from fastapi import APIRouter, HTTPException, Request, Response, status +from starlette.datastructures import State router = APIRouter(prefix="/runtime", tags=["runtime"]) @router.post("/messages") -def pull_messages(_: Request) -> Response: +def pull_messages(_: Request[State]) -> Response: """Pull messages for the ClientApp.""" raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, From 4d915cf719f7c6348921df3bfae6c5f03463dd78 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Fri, 26 Jun 2026 16:03:02 +0200 Subject: [PATCH 26/33] Improve health router --- .../py/flwr/supercore/routers/health/router.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/framework/py/flwr/supercore/routers/health/router.py b/framework/py/flwr/supercore/routers/health/router.py index b04699c1e58f..b5e899210563 100644 --- a/framework/py/flwr/supercore/routers/health/router.py +++ b/framework/py/flwr/supercore/routers/health/router.py @@ -20,25 +20,13 @@ router = APIRouter(tags=["health"]) -@router.get("/health") +@router.api_route("/health", methods=["GET", "HEAD"]) async def health() -> Response: """Report whether the API server is healthy.""" return Response(status_code=status.HTTP_200_OK) -@router.head("/health") -async def health_head() -> Response: - """Report whether the API server is healthy.""" - return Response(status_code=status.HTTP_200_OK) - - -@router.get("/ready") +@router.api_route("/ready", methods=["GET", "HEAD"]) async def ready() -> Response: """Report whether the API server is ready.""" return Response(status_code=status.HTTP_200_OK) - - -@router.head("/ready") -async def ready_head() -> Response: - """Report whether the API server is ready.""" - return Response(status_code=status.HTTP_200_OK) From 00bdc168a3e8a544cf02d0b03d72066aa7e83387 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Fri, 26 Jun 2026 16:05:27 +0200 Subject: [PATCH 27/33] Update health router --- framework/py/flwr/supercore/routers/health/router.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/py/flwr/supercore/routers/health/router.py b/framework/py/flwr/supercore/routers/health/router.py index b5e899210563..7548c89d2687 100644 --- a/framework/py/flwr/supercore/routers/health/router.py +++ b/framework/py/flwr/supercore/routers/health/router.py @@ -16,17 +16,18 @@ from fastapi import APIRouter, Response, status +from starlette.datastructures import State router = APIRouter(tags=["health"]) @router.api_route("/health", methods=["GET", "HEAD"]) -async def health() -> Response: +async def health(_: Request[State]) -> Response: """Report whether the API server is healthy.""" return Response(status_code=status.HTTP_200_OK) @router.api_route("/ready", methods=["GET", "HEAD"]) -async def ready() -> Response: +async def ready(_: Request[State]) -> Response: """Report whether the API server is ready.""" return Response(status_code=status.HTTP_200_OK) From 8cdee186e81ec8d14a52ad849cc85ddad6d8e6f1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Fri, 26 Jun 2026 16:06:06 +0200 Subject: [PATCH 28/33] Update health router --- framework/py/flwr/supercore/routers/health/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/py/flwr/supercore/routers/health/router.py b/framework/py/flwr/supercore/routers/health/router.py index 7548c89d2687..eb5afdb6d29e 100644 --- a/framework/py/flwr/supercore/routers/health/router.py +++ b/framework/py/flwr/supercore/routers/health/router.py @@ -15,7 +15,7 @@ """Health API router implementation.""" -from fastapi import APIRouter, Response, status +from fastapi import APIRouter, Request, Response, status from starlette.datastructures import State router = APIRouter(tags=["health"]) From ebfb63fc78d03310ad3e69964d3709beb8eb842c Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Fri, 26 Jun 2026 21:22:54 +0200 Subject: [PATCH 29/33] refactor(framework): Create SuperLinkLifespanConfig --- .../py/flwr/superlink/cli/flower_superlink.py | 165 ++++++++++++------ .../superlink/cli/flower_superlink_test.py | 48 ++++- 2 files changed, 159 insertions(+), 54 deletions(-) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index c42021d8751c..d6dc8b9eccb0 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -14,6 +14,7 @@ # ============================================================================== """`flower-superlink` command.""" +# pylint: disable=too-many-lines import argparse import importlib.util @@ -22,6 +23,7 @@ import sys import threading from collections.abc import Callable, Sequence +from dataclasses import dataclass from logging import INFO, WARN from pathlib import Path from time import sleep @@ -209,11 +211,37 @@ def _get_objectstore_linkstate_factories( return objectstore_factory, state_factory -# pylint: disable=too-many-branches, too-many-locals, too-many-statements -def flower_superlink() -> None: - """Run Flower SuperLink (ServerAppIo API and Fleet API).""" - warn_if_flwr_update_available(process_name="flower-superlink") +@dataclass +class SuperLinkLifespanConfig: # pylint: disable=too-many-instance-attributes + """Configuration needed to start the SuperLink lifespan.""" + + serverappio_address: str + control_address: str + health_server_address: str | None + certificates: tuple[bytes, bytes, bytes] | None + appio_certificates: tuple[bytes, bytes, bytes] | None + superexec_auth_secret: bytes | None + authn_plugin: ControlAuthnPlugin + authz_plugin: ControlAuthzPlugin + event_log_plugin: EventLogWriterPlugin | None + enable_event_log: bool + artifact_provider: ArtifactProvider | None + enable_supernode_auth: bool + fleet_api_type: str + fleet_api_address: str | None + fleet_api_num_workers: int + simulation: bool + ssl_keyfile: str | None + ssl_certfile: str | None + database: str + isolation: str + appio_ssl_ca_certfile: str | None + runtime_dependency_install: bool + +# pylint: disable=too-many-branches, too-many-locals, too-many-statements +def _parse_superlink_lifespan_config() -> SuperLinkLifespanConfig: + """Parse SuperLink CLI args and return the startup configuration.""" args = _parse_args_run_superlink().parse_args() if args.log_file: @@ -222,11 +250,6 @@ def flower_superlink() -> None: interval_hours=args.log_rotation_interval_hours, backup_count=args.log_rotation_backup_count, ) - - log(INFO, "Starting Flower SuperLink") - - event(EventType.RUN_SUPERLINK_ENTER) - # Detect if `--executor*` arguments were set if args.executor or args.executor_dir or args.executor_config: flwr_exit( @@ -314,8 +337,6 @@ def flower_superlink() -> None: # provided verify_tls_cert = not getattr(args, "disable_oidc_tls_cert_verification", None) - authn_plugin: ControlAuthnPlugin | None = None - authz_plugin: ControlAuthzPlugin | None = None event_log_plugin: EventLogWriterPlugin | None = None # Load the auth plugin if the args.account_auth_config is provided if cfg_path := getattr(args, "user_auth_config", None): @@ -386,13 +407,60 @@ def flower_superlink() -> None: f" to the Flower documentation for more information: {url_v}{page}", ) + fleet_api_address = args.fleet_api_address + if not args.simulation and not fleet_api_address: + if args.fleet_api_type in [ + TRANSPORT_TYPE_GRPC_RERE, + TRANSPORT_TYPE_GRPC_ADAPTER, + ]: + fleet_api_address = FLEET_API_GRPC_RERE_DEFAULT_ADDRESS + elif args.fleet_api_type == TRANSPORT_TYPE_REST: + fleet_api_address = FLEET_API_REST_DEFAULT_ADDRESS + + return SuperLinkLifespanConfig( + serverappio_address=serverappio_address, + control_address=control_address, + health_server_address=health_server_address, + certificates=certificates, + appio_certificates=appio_certificates, + superexec_auth_secret=superexec_auth_secret, + authn_plugin=authn_plugin, + authz_plugin=authz_plugin, + event_log_plugin=event_log_plugin, + enable_event_log=getattr(args, "enable_event_log", False), + artifact_provider=artifact_provider, + enable_supernode_auth=enable_supernode_auth, + fleet_api_type=args.fleet_api_type, + fleet_api_address=fleet_api_address, + fleet_api_num_workers=args.fleet_api_num_workers, + simulation=args.simulation, + ssl_keyfile=args.ssl_keyfile, + ssl_certfile=args.ssl_certfile, + database=args.database, + isolation=args.isolation, + appio_ssl_ca_certfile=args.appio_ssl_ca_certfile, + runtime_dependency_install=args.runtime_dependency_install, + ) + + +# pylint: disable-next=too-many-branches,too-many-locals,too-many-statements +def flower_superlink() -> None: + """Run Flower SuperLink (ServerAppIo API and Fleet API).""" + warn_if_flwr_update_available(process_name="flower-superlink") + + config = _parse_superlink_lifespan_config() + + log(INFO, "Starting Flower SuperLink") + + event(EventType.RUN_SUPERLINK_ENTER) + # Load Federation Manager - federation_manager = get_federation_manager(is_simulation=args.simulation) + federation_manager = get_federation_manager(is_simulation=config.simulation) # Initialize backend ObjectStoreFactory and StateFactory try: objectstore_factory, state_factory = _get_objectstore_linkstate_factories( - args.database, federation_manager + config.database, federation_manager ) except ValueError as err: flwr_exit(ExitCode.SUPERLINK_INVALID_ARGS, str(err)) @@ -400,45 +468,36 @@ def flower_superlink() -> None: state_factory.state() # Force initialization before starting servers # Start Control API - is_simulation = args.simulation + is_simulation = config.simulation control_server: grpc.Server = run_control_api_grpc( - address=control_address, + address=config.control_address, state_factory=state_factory, objectstore_factory=objectstore_factory, - certificates=certificates, - authn_plugin=authn_plugin, - authz_plugin=authz_plugin, - event_log_plugin=event_log_plugin, - artifact_provider=artifact_provider, - fleet_api_type=args.fleet_api_type, + certificates=config.certificates, + authn_plugin=config.authn_plugin, + authz_plugin=config.authz_plugin, + event_log_plugin=config.event_log_plugin, + artifact_provider=config.artifact_provider, + fleet_api_type=config.fleet_api_type, ) grpc_servers = [control_server] bckg_threads: list[threading.Thread] = [] # Start ServerAppIo API for both deployment and simulation runtimes. serverappio_server: grpc.Server = run_serverappio_api_grpc( - address=serverappio_address, + address=config.serverappio_address, state_factory=state_factory, objectstore_factory=objectstore_factory, - certificates=appio_certificates, - superexec_auth_secret=superexec_auth_secret, + certificates=config.appio_certificates, + superexec_auth_secret=config.superexec_auth_secret, ) grpc_servers.append(serverappio_server) # Start Fleet API if not is_simulation: - if not args.fleet_api_address: - if args.fleet_api_type in [ - TRANSPORT_TYPE_GRPC_RERE, - TRANSPORT_TYPE_GRPC_ADAPTER, - ]: - args.fleet_api_address = FLEET_API_GRPC_RERE_DEFAULT_ADDRESS - elif args.fleet_api_type == TRANSPORT_TYPE_REST: - args.fleet_api_address = FLEET_API_REST_DEFAULT_ADDRESS - - fleet_address, host, port = _format_address(args.fleet_api_address) - - num_workers = args.fleet_api_num_workers + fleet_address, host, port = _format_address(cast(str, config.fleet_api_address)) + + num_workers = config.fleet_api_num_workers if num_workers != 1: log( WARN, @@ -446,11 +505,11 @@ def flower_superlink() -> None: "You have specified %d workers. " "Support for multiple workers will be added in future releases. " "Proceeding with a single worker.", - args.fleet_api_num_workers, + config.fleet_api_num_workers, ) num_workers = 1 - if args.fleet_api_type == TRANSPORT_TYPE_REST: + if config.fleet_api_type == TRANSPORT_TYPE_REST: if ( importlib.util.find_spec("requests") and importlib.util.find_spec("starlette") @@ -463,8 +522,8 @@ def flower_superlink() -> None: args=( host, port, - args.ssl_keyfile, - args.ssl_certfile, + config.ssl_keyfile, + config.ssl_certfile, state_factory, objectstore_factory, num_workers, @@ -473,10 +532,10 @@ def flower_superlink() -> None: ) fleet_thread.start() bckg_threads.append(fleet_thread) - elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE: + elif config.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE: interceptors = [NodeAuthServerInterceptor(state_factory)] - if getattr(args, "enable_event_log", None): + if config.enable_event_log: fleet_log_plugin = _try_obtain_fleet_event_log_writer_plugin() if fleet_log_plugin is not None: interceptors.append(FleetEventLogInterceptor(fleet_log_plugin)) @@ -486,40 +545,40 @@ def flower_superlink() -> None: address=fleet_address, state_factory=state_factory, objectstore_factory=objectstore_factory, - enable_supernode_auth=enable_supernode_auth, - certificates=certificates, + enable_supernode_auth=config.enable_supernode_auth, + certificates=config.certificates, interceptors=interceptors, ) grpc_servers.append(fleet_server) - elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER: + elif config.fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER: fleet_server = _run_fleet_api_grpc_adapter( address=fleet_address, state_factory=state_factory, objectstore_factory=objectstore_factory, - certificates=certificates, + certificates=config.certificates, ) grpc_servers.append(fleet_server) else: - raise ValueError(f"Unknown fleet_api_type: {args.fleet_api_type}") + raise ValueError(f"Unknown fleet_api_type: {config.fleet_api_type}") # Launch SuperExec if isolation mode is subprocess - if args.isolation == ISOLATION_MODE_SUBPROCESS: + if config.isolation == ISOLATION_MODE_SUBPROCESS: # bound_address contains the actual address when the port is set to :0 # which means let the OS choose a free port. appio_address = resolve_bind_address(serverappio_server.bound_address) command = _get_superexec_command( appio_address=appio_address, - appio_certificates=appio_certificates, - appio_root_certificates_path=args.appio_ssl_ca_certfile, + appio_certificates=config.appio_certificates, + appio_root_certificates_path=config.appio_ssl_ca_certfile, parent_pid=os.getpid(), - runtime_dependency_install=args.runtime_dependency_install, + runtime_dependency_install=config.runtime_dependency_install, ) # pylint: disable-next=consider-using-with subprocess.Popen(command) # Launch gRPC health server - if health_server_address is not None: - health_server = run_health_server_grpc_no_tls(health_server_address) + if config.health_server_address is not None: + health_server = run_health_server_grpc_no_tls(config.health_server_address) grpc_servers.append(health_server) # Graceful shutdown diff --git a/framework/py/flwr/superlink/cli/flower_superlink_test.py b/framework/py/flwr/superlink/cli/flower_superlink_test.py index 50d2f153e7f2..5e83643a82e7 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink_test.py +++ b/framework/py/flwr/superlink/cli/flower_superlink_test.py @@ -31,7 +31,11 @@ from flwr.supercore.version import package_version from flwr.superlink.federation import NoOpFederationManager -from .flower_superlink import _obtain_superlink_certificates, _parse_args_run_superlink +from .flower_superlink import ( + _obtain_superlink_certificates, + _parse_args_run_superlink, + _parse_superlink_lifespan_config, +) app_module = importlib.import_module("flwr.superlink.cli.flower_superlink") @@ -47,6 +51,48 @@ def test_parse_superlink_log_rotation_args_defaults() -> None: assert args.log_rotation_backup_count == 7 +def test_parse_superlink_lifespan_config_returns_final_defaults( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """SuperLink CLI parsing should return the final lifespan config.""" + monkeypatch.setattr(app_module.sys, "argv", ["flower-superlink", "--insecure"]) + + config = _parse_superlink_lifespan_config() + + assert ( + config.serverappio_address == app_module.SERVERAPPIO_API_DEFAULT_SERVER_ADDRESS + ) + assert config.control_address == app_module.CONTROL_API_DEFAULT_SERVER_ADDRESS + assert config.fleet_api_address == app_module.FLEET_API_GRPC_RERE_DEFAULT_ADDRESS + assert config.health_server_address is None + assert config.certificates is None + assert config.appio_certificates is None + assert config.superexec_auth_secret is None + assert config.enable_supernode_auth is False + assert config.simulation is False + assert config.database == FLWR_IN_MEMORY_DB_NAME + + +def test_parse_superlink_lifespan_config_maps_exec_api_address( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Deprecated Exec API address should end up as Control API config.""" + monkeypatch.setattr( + app_module.sys, + "argv", + [ + "flower-superlink", + "--insecure", + "--exec-api-address", + "127.0.0.1:9099", + ], + ) + + config = _parse_superlink_lifespan_config() + + assert config.control_address == "127.0.0.1:9099" + + def test_parse_superlink_log_rotation_args_custom_values() -> None: """SuperLink log rotation args should parse explicit values.""" # Execute From 39a99194002dfb649b14a5d98a39783073cb1118 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Sat, 27 Jun 2026 14:14:04 +0200 Subject: [PATCH 30/33] refactor(framework): Introduce SuperLinkLifespan --- .../py/flwr/superlink/cli/flower_superlink.py | 364 ++++++++++++------ 1 file changed, 237 insertions(+), 127 deletions(-) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index d6dc8b9eccb0..393cfb6b77a5 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -239,6 +239,237 @@ class SuperLinkLifespanConfig: # pylint: disable=too-many-instance-attributes runtime_dependency_install: bool +class SuperLinkLifespan: # pylint: disable=too-many-instance-attributes + """Own SuperLink startup resources for the `flower-superlink` process.""" + + def __init__(self, config: SuperLinkLifespanConfig) -> None: + self.config = config + self.grpc_servers: list[grpc.Server] = [] + self.bckg_threads: list[threading.Thread] = [] + self.superexec_process: subprocess.Popen[bytes] | None = None + self.objectstore_factory: ObjectStoreFactory | None = None + self.state_factory: LinkStateFactory | None = None + self._started = False + + def startup(self) -> None: + """Start SuperLink services.""" + log(INFO, "SuperLinkLifespan: start") + if self._started: + return + + federation_manager = get_federation_manager( + is_simulation=self.config.simulation + ) + objectstore_factory, state_factory = _get_objectstore_linkstate_factories( + self.config.database, federation_manager + ) + state_factory.state() # Force initialization before starting network servers + self.objectstore_factory = objectstore_factory + self.state_factory = state_factory + + self._start_control_api() + self._start_serverappio_api() + self._start_fleet_api() + self._start_superexec_if_needed() + self._start_health_server_if_needed() + self._started = True + + def shutdown(self) -> None: + """Stop resources started by this lifespan.""" + log(INFO, "SuperLinkLifespan: stop") + if ( + not self._started + and not self.grpc_servers + and not self.bckg_threads + and self.superexec_process is None + ): + return + + for grpc_server in reversed(self.grpc_servers): + grpc_server.stop(grace=1) + + for thread in self.bckg_threads: + thread.join(timeout=1.0) + if thread.is_alive(): + log( + WARN, + "Background thread %s is still running during SuperLink " + "shutdown.", + thread.name, + ) + + if self.superexec_process is not None: + self.superexec_process.terminate() + try: + self.superexec_process.wait(timeout=1.0) + except subprocess.TimeoutExpired: + log(WARN, "SuperExec subprocess did not terminate within 1 second.") + + self.grpc_servers.clear() + self.bckg_threads.clear() + self.superexec_process = None + self._started = False + + def wait_until_background_thread_exits(self) -> None: + """Block like the historical `flower-superlink` command.""" + while all(thread.is_alive() for thread in self.bckg_threads): + sleep(0.1) + + def _start_control_api(self) -> None: + config = self.config + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + control_server: grpc.Server = run_control_api_grpc( + address=config.control_address, + state_factory=self.state_factory, + objectstore_factory=self.objectstore_factory, + certificates=config.certificates, + authn_plugin=config.authn_plugin, + authz_plugin=config.authz_plugin, + event_log_plugin=config.event_log_plugin, + artifact_provider=config.artifact_provider, + fleet_api_type=config.fleet_api_type, + ) + self.grpc_servers.append(control_server) + + def _start_serverappio_api(self) -> None: + config = self.config + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + serverappio_server: grpc.Server = run_serverappio_api_grpc( + address=config.serverappio_address, + state_factory=self.state_factory, + objectstore_factory=self.objectstore_factory, + certificates=config.appio_certificates, + superexec_auth_secret=config.superexec_auth_secret, + ) + self.grpc_servers.append(serverappio_server) + + def _start_fleet_api(self) -> None: + config = self.config + if config.simulation: + return + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + fleet_api_address = config.fleet_api_address + if not fleet_api_address: + if config.fleet_api_type in [ + TRANSPORT_TYPE_GRPC_RERE, + TRANSPORT_TYPE_GRPC_ADAPTER, + ]: + fleet_api_address = FLEET_API_GRPC_RERE_DEFAULT_ADDRESS + elif config.fleet_api_type == TRANSPORT_TYPE_REST: + fleet_api_address = FLEET_API_REST_DEFAULT_ADDRESS + + fleet_address, host, port = _format_address(cast(str, fleet_api_address)) + num_workers = config.fleet_api_num_workers + if num_workers != 1: + log( + WARN, + "The Fleet API currently supports only 1 worker. " + "You have specified %d workers. " + "Support for multiple workers will be added in future releases. " + "Proceeding with a single worker.", + config.fleet_api_num_workers, + ) + num_workers = 1 + + if config.fleet_api_type == TRANSPORT_TYPE_REST: + self._start_fleet_rest_api(host, port, num_workers) + elif config.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE: + self._start_fleet_grpc_rere(fleet_address) + elif config.fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER: + self._start_fleet_grpc_adapter(fleet_address) + else: + raise ValueError(f"Unknown fleet_api_type: {config.fleet_api_type}") + + def _start_fleet_rest_api(self, host: str, port: int, num_workers: int) -> None: + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + if ( + importlib.util.find_spec("requests") + and importlib.util.find_spec("starlette") + and importlib.util.find_spec("uvicorn") + ) is None: + flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST) + + fleet_thread = threading.Thread( + target=_run_fleet_api_rest, + args=( + host, + port, + self.config.ssl_keyfile, + self.config.ssl_certfile, + self.state_factory, + self.objectstore_factory, + num_workers, + ), + daemon=True, + ) + fleet_thread.start() + self.bckg_threads.append(fleet_thread) + + def _start_fleet_grpc_rere(self, fleet_address: str) -> None: + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + interceptors = [NodeAuthServerInterceptor(self.state_factory)] + if self.config.enable_event_log: + fleet_log_plugin = _try_obtain_fleet_event_log_writer_plugin() + if fleet_log_plugin is not None: + interceptors.append(FleetEventLogInterceptor(fleet_log_plugin)) + log(INFO, "Flower Fleet event logging enabled") + + fleet_server = _run_fleet_api_grpc_rere( + address=fleet_address, + state_factory=self.state_factory, + objectstore_factory=self.objectstore_factory, + enable_supernode_auth=self.config.enable_supernode_auth, + certificates=self.config.certificates, + interceptors=interceptors, + ) + self.grpc_servers.append(fleet_server) + + def _start_fleet_grpc_adapter(self, fleet_address: str) -> None: + if self.state_factory is None or self.objectstore_factory is None: + raise RuntimeError("SuperLink lifespan state has not been initialized.") + + fleet_server = _run_fleet_api_grpc_adapter( + address=fleet_address, + state_factory=self.state_factory, + objectstore_factory=self.objectstore_factory, + certificates=self.config.certificates, + ) + self.grpc_servers.append(fleet_server) + + def _start_superexec_if_needed(self) -> None: + config = self.config + if config.isolation != ISOLATION_MODE_SUBPROCESS: + return + + serverappio_server = self.grpc_servers[1] + appio_address = resolve_bind_address(serverappio_server.bound_address) + command = _get_superexec_command( + appio_address=appio_address, + appio_certificates=config.appio_certificates, + appio_root_certificates_path=config.appio_ssl_ca_certfile, + parent_pid=os.getpid(), + runtime_dependency_install=config.runtime_dependency_install, + ) + # pylint: disable-next=consider-using-with + self.superexec_process = subprocess.Popen(command) + + def _start_health_server_if_needed(self) -> None: + if self.config.health_server_address is None: + return + + health_server = run_health_server_grpc_no_tls(self.config.health_server_address) + self.grpc_servers.append(health_server) + + # pylint: disable=too-many-branches, too-many-locals, too-many-statements def _parse_superlink_lifespan_config() -> SuperLinkLifespanConfig: """Parse SuperLink CLI args and return the startup configuration.""" @@ -443,7 +674,6 @@ def _parse_superlink_lifespan_config() -> SuperLinkLifespanConfig: ) -# pylint: disable-next=too-many-branches,too-many-locals,too-many-statements def flower_superlink() -> None: """Run Flower SuperLink (ServerAppIo API and Fleet API).""" warn_if_flwr_update_available(process_name="flower-superlink") @@ -454,143 +684,23 @@ def flower_superlink() -> None: event(EventType.RUN_SUPERLINK_ENTER) - # Load Federation Manager - federation_manager = get_federation_manager(is_simulation=config.simulation) - - # Initialize backend ObjectStoreFactory and StateFactory + lifespan = SuperLinkLifespan(config) try: - objectstore_factory, state_factory = _get_objectstore_linkstate_factories( - config.database, federation_manager - ) + lifespan.startup() except ValueError as err: + lifespan.shutdown() flwr_exit(ExitCode.SUPERLINK_INVALID_ARGS, str(err)) - state_factory.state() # Force initialization before starting servers - - # Start Control API - is_simulation = config.simulation - control_server: grpc.Server = run_control_api_grpc( - address=config.control_address, - state_factory=state_factory, - objectstore_factory=objectstore_factory, - certificates=config.certificates, - authn_plugin=config.authn_plugin, - authz_plugin=config.authz_plugin, - event_log_plugin=config.event_log_plugin, - artifact_provider=config.artifact_provider, - fleet_api_type=config.fleet_api_type, - ) - grpc_servers = [control_server] - bckg_threads: list[threading.Thread] = [] - - # Start ServerAppIo API for both deployment and simulation runtimes. - serverappio_server: grpc.Server = run_serverappio_api_grpc( - address=config.serverappio_address, - state_factory=state_factory, - objectstore_factory=objectstore_factory, - certificates=config.appio_certificates, - superexec_auth_secret=config.superexec_auth_secret, - ) - grpc_servers.append(serverappio_server) - - # Start Fleet API - if not is_simulation: - fleet_address, host, port = _format_address(cast(str, config.fleet_api_address)) - - num_workers = config.fleet_api_num_workers - if num_workers != 1: - log( - WARN, - "The Fleet API currently supports only 1 worker. " - "You have specified %d workers. " - "Support for multiple workers will be added in future releases. " - "Proceeding with a single worker.", - config.fleet_api_num_workers, - ) - num_workers = 1 - - if config.fleet_api_type == TRANSPORT_TYPE_REST: - if ( - importlib.util.find_spec("requests") - and importlib.util.find_spec("starlette") - and importlib.util.find_spec("uvicorn") - ) is None: - flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST) - - fleet_thread = threading.Thread( - target=_run_fleet_api_rest, - args=( - host, - port, - config.ssl_keyfile, - config.ssl_certfile, - state_factory, - objectstore_factory, - num_workers, - ), - daemon=True, - ) - fleet_thread.start() - bckg_threads.append(fleet_thread) - elif config.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE: - - interceptors = [NodeAuthServerInterceptor(state_factory)] - if config.enable_event_log: - fleet_log_plugin = _try_obtain_fleet_event_log_writer_plugin() - if fleet_log_plugin is not None: - interceptors.append(FleetEventLogInterceptor(fleet_log_plugin)) - log(INFO, "Flower Fleet event logging enabled") - - fleet_server = _run_fleet_api_grpc_rere( - address=fleet_address, - state_factory=state_factory, - objectstore_factory=objectstore_factory, - enable_supernode_auth=config.enable_supernode_auth, - certificates=config.certificates, - interceptors=interceptors, - ) - grpc_servers.append(fleet_server) - elif config.fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER: - fleet_server = _run_fleet_api_grpc_adapter( - address=fleet_address, - state_factory=state_factory, - objectstore_factory=objectstore_factory, - certificates=config.certificates, - ) - grpc_servers.append(fleet_server) - else: - raise ValueError(f"Unknown fleet_api_type: {config.fleet_api_type}") - - # Launch SuperExec if isolation mode is subprocess - if config.isolation == ISOLATION_MODE_SUBPROCESS: - # bound_address contains the actual address when the port is set to :0 - # which means let the OS choose a free port. - appio_address = resolve_bind_address(serverappio_server.bound_address) - command = _get_superexec_command( - appio_address=appio_address, - appio_certificates=config.appio_certificates, - appio_root_certificates_path=config.appio_ssl_ca_certfile, - parent_pid=os.getpid(), - runtime_dependency_install=config.runtime_dependency_install, - ) - # pylint: disable-next=consider-using-with - subprocess.Popen(command) - - # Launch gRPC health server - if config.health_server_address is not None: - health_server = run_health_server_grpc_no_tls(config.health_server_address) - grpc_servers.append(health_server) - # Graceful shutdown register_signal_handlers( event_type=EventType.RUN_SUPERLINK_LEAVE, exit_message="SuperLink terminated gracefully.", - grpc_servers=grpc_servers, + grpc_servers=lifespan.grpc_servers, + exit_handlers=[lifespan.shutdown], ) # Block until a thread exits prematurely - while all(thread.is_alive() for thread in bckg_threads): - sleep(0.1) + lifespan.wait_until_background_thread_exits() # Exit if any thread has exited prematurely # This code will not be reached if the SuperLink stops gracefully From cb015f158c203dae3f80a6ae8d1df0226e6e3327 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Tue, 30 Jun 2026 10:28:22 +0100 Subject: [PATCH 31/33] Apply suggestion from @panh99 --- framework/py/flwr/superlink/cli/flower_superlink.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/py/flwr/superlink/cli/flower_superlink.py b/framework/py/flwr/superlink/cli/flower_superlink.py index 9b083b2d9a34..d2768dc505e6 100644 --- a/framework/py/flwr/superlink/cli/flower_superlink.py +++ b/framework/py/flwr/superlink/cli/flower_superlink.py @@ -806,9 +806,9 @@ def _run_superlink_http_api( # pylint: disable=import-outside-toplevel except ModuleNotFoundError: flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST) - superlink_lifespan = ( - SuperLinkLifespan(lifespan_config) if start_legacy_grpc else None - ) + superlink_lifespan = None + if start_legacy_grpc: + superlink_lifespan = SuperLinkLifespan(lifespan_config) fastapi_app = create_app( superlink_lifespan=superlink_lifespan, start_legacy_grpc=start_legacy_grpc, From 9f9864faa3e3d67f20a9abec907377bddd3ff407 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Tue, 30 Jun 2026 11:47:38 +0100 Subject: [PATCH 32/33] Apply suggestion from @panh99 --- framework/py/flwr/superlink/routers/runtime/router.py | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/py/flwr/superlink/routers/runtime/router.py b/framework/py/flwr/superlink/routers/runtime/router.py index c2922a6e5369..73ce3202cfa9 100644 --- a/framework/py/flwr/superlink/routers/runtime/router.py +++ b/framework/py/flwr/superlink/routers/runtime/router.py @@ -14,6 +14,7 @@ # ============================================================================== """Runtime API router.""" + from fastapi import APIRouter, HTTPException, Response, status router = APIRouter(prefix="/runtime", tags=["runtime"]) From 9ea37288e18351fc8de8d66b0507f9aa1fe66782 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Tue, 30 Jun 2026 11:48:18 +0100 Subject: [PATCH 33/33] Apply suggestion from @panh99 --- framework/py/flwr/supernode/routers/runtime/router.py | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/py/flwr/supernode/routers/runtime/router.py b/framework/py/flwr/supernode/routers/runtime/router.py index 1f09b4c59757..321123b107a8 100644 --- a/framework/py/flwr/supernode/routers/runtime/router.py +++ b/framework/py/flwr/supernode/routers/runtime/router.py @@ -14,6 +14,7 @@ # ============================================================================== """Runtime API router.""" + from fastapi import APIRouter, HTTPException, Response, status router = APIRouter(prefix="/runtime", tags=["runtime"])