From 32b994ab120238662068bd365a59da34a1484f78 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 12 May 2026 14:09:57 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Let=20backend=20handle?= =?UTF-8?q?=20new=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shortcake-Parent: main --- pyproject.toml | 2 +- src/fastapi_cloud_cli/commands/deploy.py | 4 ++-- src/fastapi_cloud_cli/utils/rich_ansi.py | 19 +++++++++++++++++++ tests/test_rich_ansi.py | 20 ++++++++++++++++++++ uv.lock | 8 ++++---- 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/fastapi_cloud_cli/utils/rich_ansi.py create mode 100644 tests/test_rich_ansi.py diff --git a/pyproject.toml b/pyproject.toml index c6d0ab2..cb1b07c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "uvicorn[standard] >= 0.17.6", "rignore >= 0.5.1", "httpx >= 0.27.0", - "rich-toolkit >= 0.19.7", + "rich-toolkit>=0.19.9", "pydantic[email] >= 2.7.4; python_version < '3.13'", "pydantic[email] >= 2.8.0; python_version == '3.13'", "pydantic[email] >= 2.12.0; python_version >= '3.14'", diff --git a/src/fastapi_cloud_cli/commands/deploy.py b/src/fastapi_cloud_cli/commands/deploy.py index f903a90..9fa7044 100644 --- a/src/fastapi_cloud_cli/commands/deploy.py +++ b/src/fastapi_cloud_cli/commands/deploy.py @@ -14,7 +14,6 @@ import typer from httpx import Client from pydantic import AfterValidator, BaseModel, EmailStr, TypeAdapter, ValidationError -from rich.text import Text from rich_toolkit import RichToolkit from rich_toolkit.menu import Option from rich_toolkit.progress import Progress @@ -31,6 +30,7 @@ from fastapi_cloud_cli.utils.auth import Identity from fastapi_cloud_cli.utils.cli import get_rich_toolkit from fastapi_cloud_cli.utils.progress_file import ProgressFile +from fastapi_cloud_cli.utils.rich_ansi import text_from_ansi logger = logging.getLogger(__name__) @@ -518,7 +518,7 @@ def _wait_for_deployment( time_elapsed = time.monotonic() - started_at if log.type == "message": - progress.log(Text.from_ansi(log.message.rstrip())) # ty: ignore[unresolved-attribute] + progress.log(text_from_ansi(log.message)) # ty: ignore[unresolved-attribute] if log.type == "complete": build_complete = True diff --git a/src/fastapi_cloud_cli/utils/rich_ansi.py b/src/fastapi_cloud_cli/utils/rich_ansi.py new file mode 100644 index 0000000..186fd9e --- /dev/null +++ b/src/fastapi_cloud_cli/utils/rich_ansi.py @@ -0,0 +1,19 @@ +import re + +from rich.ansi import AnsiDecoder +from rich.text import Text + +ANSI_NEWLINE_SPLIT_RE = re.compile(r"(?<=\n)") + + +def text_from_ansi(text: str) -> Text: + # Text.from_ansi only preserves newlines in Rich 15, in order to + # avoid forcing everyone to use Rich 15 we handle this ourselves by splitting the text on newlines, + # decoding each line separately, then join them back together with newlines. + joiner = Text("\n") + decoder = AnsiDecoder() + + return joiner.join( + decoder.decode_line(line.rstrip("\n")) + for line in ANSI_NEWLINE_SPLIT_RE.split(text) + ) diff --git a/tests/test_rich_ansi.py b/tests/test_rich_ansi.py new file mode 100644 index 0000000..5ad2d1a --- /dev/null +++ b/tests/test_rich_ansi.py @@ -0,0 +1,20 @@ +from fastapi_cloud_cli.utils.rich_ansi import text_from_ansi + + +def test_text_from_ansi_preserves_newlines() -> None: + assert text_from_ansi("").plain == "" + assert text_from_ansi("\n").plain == "\n" + assert text_from_ansi("\n\n").plain == "\n\n" + assert text_from_ansi("Hello").plain == "Hello" + assert text_from_ansi("Hello\n").plain == "Hello\n" + assert text_from_ansi("Hello\n\n").plain == "Hello\n\n" + assert text_from_ansi("Hello\n World").plain == "Hello\n World" + assert text_from_ansi("Hello\n\n World").plain == "Hello\n\n World" + assert text_from_ansi("Hello\n World\n").plain == "Hello\n World\n" + + +def test_text_from_ansi_handles_carriage_returns_per_line() -> None: + assert text_from_ansi("start\rend").plain == "end" + assert text_from_ansi("start\rend\n").plain == "end\n" + assert text_from_ansi("one\rtwo\nthree").plain == "two\nthree" + assert text_from_ansi("one\ntwo\rthree\n").plain == "one\nthree\n" diff --git a/uv.lock b/uv.lock index 4f153e4..0a06a00 100644 --- a/uv.lock +++ b/uv.lock @@ -259,7 +259,7 @@ requires-dist = [ { name = "pydantic", extras = ["email"], marker = "python_full_version < '3.13'", specifier = ">=2.7.4" }, { name = "pydantic", extras = ["email"], marker = "python_full_version == '3.13.*'", specifier = ">=2.8.0" }, { name = "pydantic", extras = ["email"], marker = "python_full_version >= '3.14'", specifier = ">=2.12.0" }, - { name = "rich-toolkit", specifier = ">=0.19.7" }, + { name = "rich-toolkit", specifier = ">=0.19.8" }, { name = "rignore", specifier = ">=0.5.1" }, { name = "sentry-sdk", specifier = ">=2.20.0" }, { name = "typer", specifier = ">=0.16.0" }, @@ -881,16 +881,16 @@ wheels = [ [[package]] name = "rich-toolkit" -version = "0.19.7" +version = "0.19.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/77/45030521529b1c4e34d4dbbdfb6dd81cd39e44539b122ef55ade5dab071d/rich_toolkit-0.19.8.tar.gz", hash = "sha256:4cfd2bcb34299442168c983af22e74c881e055e8b67417f577307bf0eaa4d0af", size = 196485, upload-time = "2026-05-12T13:01:46.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, + { url = "https://files.pythonhosted.org/packages/69/4c/998d76e1c35fb70493d4300fe625ad933b40fd225de58882411fd4dc30e8/rich_toolkit-0.19.8-py3-none-any.whl", hash = "sha256:97126e170b95ca357e034bf729da408f9eb37c5627183807962bc18f18771033", size = 33174, upload-time = "2026-05-12T13:01:48.185Z" }, ] [[package]] From 62f3d3a5d89c95a0fce734e1749f32d26cc3f82c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 May 2026 11:41:31 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uv.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 0a06a00..784a228 100644 --- a/uv.lock +++ b/uv.lock @@ -259,7 +259,7 @@ requires-dist = [ { name = "pydantic", extras = ["email"], marker = "python_full_version < '3.13'", specifier = ">=2.7.4" }, { name = "pydantic", extras = ["email"], marker = "python_full_version == '3.13.*'", specifier = ">=2.8.0" }, { name = "pydantic", extras = ["email"], marker = "python_full_version >= '3.14'", specifier = ">=2.12.0" }, - { name = "rich-toolkit", specifier = ">=0.19.8" }, + { name = "rich-toolkit", specifier = ">=0.19.9" }, { name = "rignore", specifier = ">=0.5.1" }, { name = "sentry-sdk", specifier = ">=2.20.0" }, { name = "typer", specifier = ">=0.16.0" }, @@ -881,16 +881,16 @@ wheels = [ [[package]] name = "rich-toolkit" -version = "0.19.8" +version = "0.19.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/77/45030521529b1c4e34d4dbbdfb6dd81cd39e44539b122ef55ade5dab071d/rich_toolkit-0.19.8.tar.gz", hash = "sha256:4cfd2bcb34299442168c983af22e74c881e055e8b67417f577307bf0eaa4d0af", size = 196485, upload-time = "2026-05-12T13:01:46.976Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/10/dc6e64e85244971671981dc26b09353a1564f5e61b977c80180dc42ad90b/rich_toolkit-0.19.9.tar.gz", hash = "sha256:fce5c6f41f79382ecf60a79851b2543f627568e3e07c78ab4b8542e1ca247d1c", size = 197653, upload-time = "2026-05-13T09:55:04.286Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/4c/998d76e1c35fb70493d4300fe625ad933b40fd225de58882411fd4dc30e8/rich_toolkit-0.19.8-py3-none-any.whl", hash = "sha256:97126e170b95ca357e034bf729da408f9eb37c5627183807962bc18f18771033", size = 33174, upload-time = "2026-05-12T13:01:48.185Z" }, + { url = "https://files.pythonhosted.org/packages/8f/60/5a7de329d0b5b619757c169bbf8a5146c20fe49bd4d74045937fcd45a7d0/rich_toolkit-0.19.9-py3-none-any.whl", hash = "sha256:a1341f88feed5f295f001bb1c6b6cf1e208674187dd900416a30fd9d6f74fcce", size = 33711, upload-time = "2026-05-13T09:55:05.345Z" }, ] [[package]]