From 5c3bc87455fdad6d8519643f8da6c570aa015928 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 10:54:31 +0200 Subject: [PATCH 1/8] refactor(utils): add root callback to prepare_cli Co-Authored-By: Claude Opus 5.5 --- src/aignostics/utils/CLAUDE.md | 1 + src/aignostics/utils/_cli.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/aignostics/utils/CLAUDE.md b/src/aignostics/utils/CLAUDE.md index 7014389bc..05f292e16 100644 --- a/src/aignostics/utils/CLAUDE.md +++ b/src/aignostics/utils/CLAUDE.md @@ -24,6 +24,7 @@ The utils module provides core infrastructure and shared utilities used across a - `_health.py` - Health check framework - `_user_agent.py` - Enhanced user agent generation with CI/CD context - `boot.py` - Application bootstrap and initialization +- `_cli.py` - `prepare_cli(cli, epilog, *, root_callback=None)` registers discovered subcommand typers, sets the epilog, and applies the no-args-is-help workaround to every typer. The optional `root_callback(ctx)` runs in the root callback only (not in subcommand callbacks), before the workaround. A Typer instance has only one callback, so use `root_callback` instead of `cli.callback(...)` in the root CLI. **System Utilities:** diff --git a/src/aignostics/utils/_cli.py b/src/aignostics/utils/_cli.py index 94230fc3b..ee2b5e411 100644 --- a/src/aignostics/utils/_cli.py +++ b/src/aignostics/utils/_cli.py @@ -1,6 +1,7 @@ """Command-line interface utilities.""" import sys +from collections.abc import Callable from pathlib import Path import typer @@ -8,13 +9,22 @@ from ._di import locate_implementations -def prepare_cli(cli: typer.Typer, epilog: str) -> None: +def prepare_cli( + cli: typer.Typer, + epilog: str, + *, + root_callback: Callable[[typer.Context], None] | None = None, +) -> None: """ Dynamically locate, register and prepare subcommands. Args: cli (typer.Typer): Typer instance epilog (str): Epilog to add + root_callback (Callable[[typer.Context], None] | None): Optional function that the root + callback of ``cli`` calls with the Typer context before the no-args-is-help workaround. + Subcommand callbacks do not call it. If ``None``, the root callback only applies the + workaround. """ for subcli in locate_implementations(typer.Typer): if subcli != cli: @@ -29,6 +39,10 @@ def prepare_cli(cli: typer.Typer, epilog: str) -> None: if not any(arg.endswith("typer") for arg in Path(sys.argv[0]).parts): _add_epilog_recursively(cli, epilog) + # add root callback, which also applies the no_args_is_help workaround + if root_callback is not None and not hasattr(cli, "no_args_callback_added"): + _add_root_callback(cli, root_callback) + # add no_args_is_help for all subcommands _no_args_is_help_recursively(cli) @@ -44,6 +58,23 @@ def no_args_is_help_workaround(ctx: typer.Context) -> None: raise typer.Exit +def _add_root_callback(cli: typer.Typer, root_callback: Callable[[typer.Context], None]) -> None: + """ + Register a root callback that calls root_callback, then the no-args-is-help workaround. + + Args: + cli (typer.Typer): Root Typer instance + root_callback (Callable[[typer.Context], None]): Function to call first with the Typer context + """ + + def _root_callback(ctx: typer.Context) -> None: + root_callback(ctx) + no_args_is_help_workaround(ctx) + + cli.callback(invoke_without_command=True)(_root_callback) + cli.no_args_callback_added = True # type: ignore[attr-defined] + + def _add_epilog_recursively(cli: typer.Typer, epilog: str) -> None: """ Add epilog to all typers in the tree. From d320b287b6c2f7fd4782bd28376fb1a01524e9cd Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 11:00:03 +0200 Subject: [PATCH 2/8] feat: log deprecation notice on import Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 14 ++++ src/aignostics/__init__.py | 12 +++ src/aignostics/constants.py | 9 +++ src/aignostics/gui/_frame.py | 4 +- tests/aignostics/constants_test.py | 13 ++++ tests/aignostics/deprecation_test.py | 111 +++++++++++++++++++++++++++ 6 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 tests/aignostics/constants_test.py create mode 100644 tests/aignostics/deprecation_test.py diff --git a/CLAUDE.md b/CLAUDE.md index 9d9a2eb0e..6dd9da5b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -486,6 +486,20 @@ def action_command(param: str): ## Important Notes +### Deprecation notice + +The SDK is deprecated. `aignostics.constants.DEPRECATION_NOTICE` is the single +source of the notice text. In code, use the constant. Do not copy the text. +These surfaces show it: + +* **Log** — in library mode, import of `aignostics` writes it as a `WARNING` log line + and emits a `FutureWarning`. Child processes do not repeat it. The CLI does not log it. +* **CLI** — the root help text and stderr before each command. +* **Launchpad** — a banner below the header of each page. +* **Docs site** — a Furo announcement banner. + +`README.md` (and so the PyPI page) also shows the notice and a migration table. + ### Module Loading `nicegui`, `marimo`, and `ijson` are core dependencies, so modules using them diff --git a/src/aignostics/__init__.py b/src/aignostics/__init__.py index dcf7afbe5..6b0005c25 100644 --- a/src/aignostics/__init__.py +++ b/src/aignostics/__init__.py @@ -1,5 +1,6 @@ """Python SDK providing access to Aignostics AI services.""" +import multiprocessing import os import warnings from typing import Any @@ -7,13 +8,17 @@ # TODO(Helmut): remove when google_crc32c supports Python 3.14 warnings.filterwarnings("ignore", message="As the c extension couldn't be imported", category=RuntimeWarning) +from loguru import logger # noqa: E402 + from .constants import ( # noqa: E402 + DEPRECATION_NOTICE, HETA_APPLICATION_ID, SENTRY_INTEGRATIONS, TEST_APP_APPLICATION_ID, WSI_SUPPORTED_FILE_EXTENSIONS, WSI_SUPPORTED_FILE_EXTENSIONS_TEST_APP, ) +from .utils import __is_library_mode__ # noqa: E402 from .utils.boot import boot # noqa: E402 # Add scheme to HTTP proxy environment variables if missing @@ -42,7 +47,14 @@ def _log_filter(record: Any) -> bool: # noqa: ANN401 boot(sentry_integrations=SENTRY_INTEGRATIONS, log_filter=_log_filter) +# Warn library users once per process tree. The CLI shows its own notice, and child processes +# (for example of the Launchpad) import the package again. +if __is_library_mode__ and multiprocessing.parent_process() is None: + logger.warning("{}", DEPRECATION_NOTICE) + warnings.warn(DEPRECATION_NOTICE, FutureWarning, stacklevel=2) + __all__ = [ + "DEPRECATION_NOTICE", "HETA_APPLICATION_ID", "TEST_APP_APPLICATION_ID", "WSI_SUPPORTED_FILE_EXTENSIONS", diff --git a/src/aignostics/constants.py b/src/aignostics/constants.py index 8acd926b0..5dec6e89b 100644 --- a/src/aignostics/constants.py +++ b/src/aignostics/constants.py @@ -39,5 +39,14 @@ WSI_SUPPORTED_FILE_EXTENSIONS_TEST_APP = {".tiff"} WINDOW_TITLE = "Aignostics Launchpad" + +# Deprecation of the Aignostics Python SDK +PLATFORM_API_DOCS_URL = "https://platform.aignostics.com/api/v1/docs" +CONSOLE_URL = "https://platform.aignostics.com" +DEPRECATION_NOTICE = ( + "The Aignostics Python SDK is deprecated and gets only bug fixes and security fixes until April 30, 2027. " + "After that date, the repository and the PyPI project are archived. " + f"Use the Aignostics Platform REST API ({PLATFORM_API_DOCS_URL}) or the Console ({CONSOLE_URL})." +) # Organizations with internal/advanced access (e.g., platform-wide queue visibility, GPU config) INTERNAL_ORGS = {"aignostics", "pre-alpha-org", "lmu", "charite"} diff --git a/src/aignostics/gui/_frame.py b/src/aignostics/gui/_frame.py index 8488e63e1..0bbf83222 100644 --- a/src/aignostics/gui/_frame.py +++ b/src/aignostics/gui/_frame.py @@ -14,7 +14,7 @@ from humanize import naturaldelta from loguru import logger -from aignostics.constants import WINDOW_TITLE +from aignostics.constants import CONSOLE_URL, WINDOW_TITLE from aignostics.utils import __version__, open_user_data_directory from ._theme import theme @@ -337,7 +337,7 @@ def toggle_dark_mode() -> None: with ui.item_section().props("avatar"): ui.icon("domain", color="primary") with ui.item_section(): - ui.link("Go to Console", "https://platform.aignostics.com", new_tab=True).mark("LINK_PLATFORM") + ui.link("Go to Console", CONSOLE_URL, new_tab=True).mark("LINK_PLATFORM") with ui.item().props("clickable"): with ui.item_section().props("avatar"): ui.icon("local_library", color="primary") diff --git a/tests/aignostics/constants_test.py b/tests/aignostics/constants_test.py new file mode 100644 index 000000000..f34b66beb --- /dev/null +++ b/tests/aignostics/constants_test.py @@ -0,0 +1,13 @@ +"""Tests for the project constants in aignostics.constants.""" + +import pytest + +from aignostics.constants import CONSOLE_URL, DEPRECATION_NOTICE, PLATFORM_API_DOCS_URL + + +@pytest.mark.unit +def test_deprecation_notice_names_end_date_and_alternatives() -> None: + """The deprecation notice names the end of support and both alternatives.""" + assert "April 30, 2027" in DEPRECATION_NOTICE + assert PLATFORM_API_DOCS_URL in DEPRECATION_NOTICE + assert CONSOLE_URL in DEPRECATION_NOTICE diff --git a/tests/aignostics/deprecation_test.py b/tests/aignostics/deprecation_test.py new file mode 100644 index 000000000..c85ccc403 --- /dev/null +++ b/tests/aignostics/deprecation_test.py @@ -0,0 +1,111 @@ +"""Tests for the deprecation notice that the SDK shows on import.""" + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from aignostics.constants import DEPRECATION_NOTICE + +ENV_WITHOUT_PYTEST_FLAG = {key: value for key, value in os.environ.items() if key != "PYTEST_RUNNING_AIGNOSTICS"} + + +def _import_in_library_mode() -> subprocess.CompletedProcess[str]: + """Import aignostics in a fresh Python process, outside of pytest mode. + + Returns: + subprocess.CompletedProcess[str]: The completed process with captured output. + """ + return subprocess.run( + [sys.executable, "-W", "always::FutureWarning", "-c", "import aignostics"], + capture_output=True, + text=True, + check=False, + shell=False, + encoding="utf-8", + env=ENV_WITHOUT_PYTEST_FLAG, + ) + + +@pytest.mark.integration +@pytest.mark.timeout(timeout=60) +def test_import_in_library_mode_emits_future_warning() -> None: + """Import in library mode emits a FutureWarning with the deprecation notice.""" + result = _import_in_library_mode() + + assert result.returncode == 0, result.stderr + assert f"FutureWarning: {DEPRECATION_NOTICE}" in result.stderr + + +@pytest.mark.integration +@pytest.mark.timeout(timeout=60) +def test_import_in_library_mode_logs_deprecation_warning() -> None: + """Import in library mode writes the deprecation notice as a WARNING log line.""" + result = _import_in_library_mode() + + assert result.returncode == 0, result.stderr + log_lines = [line for line in result.stderr.splitlines() if "FutureWarning" not in line] + assert any("WARNING" in line and DEPRECATION_NOTICE in line for line in log_lines), result.stderr + + +@pytest.mark.integration +@pytest.mark.timeout(timeout=60) +def test_cli_mode_shows_notice_once_without_future_warning() -> None: + """The CLI shows the deprecation notice once on stderr, without a log line or a FutureWarning.""" + script_name = "aignostics.exe" if sys.platform == "win32" else "aignostics" + cli_script = Path(sys.executable).parent / script_name + + result = subprocess.run( + [str(cli_script), "system", "--help"], + capture_output=True, + text=True, + check=False, + shell=False, + encoding="utf-8", + env=ENV_WITHOUT_PYTEST_FLAG, + ) + + assert result.returncode == 0, result.stderr + assert result.stderr.count(DEPRECATION_NOTICE) == 1, result.stderr + assert "WARNING" not in result.stderr + assert "FutureWarning" not in result.stderr + + +@pytest.mark.integration +@pytest.mark.timeout(timeout=60) +def test_import_in_child_process_shows_no_notice(tmp_path: Path) -> None: + """A child process that imports aignostics does not repeat the deprecation notice.""" + script = tmp_path / "spawn_child.py" + script.write_text( + "import multiprocessing\n" + "\n" + "\n" + "def child() -> None:\n" + " import aignostics # noqa: F401\n" + "\n" + ' print("CHILD_IMPORTED", flush=True)\n' + "\n" + "\n" + 'if __name__ == "__main__":\n' + ' process = multiprocessing.get_context("spawn").Process(target=child)\n' + " process.start()\n" + " process.join()\n" + " raise SystemExit(process.exitcode)\n", + encoding="utf-8", + ) + + result = subprocess.run( + [sys.executable, "-W", "always::FutureWarning", str(script)], + capture_output=True, + text=True, + check=False, + shell=False, + encoding="utf-8", + env=ENV_WITHOUT_PYTEST_FLAG, + ) + + assert result.returncode == 0, result.stderr + assert "CHILD_IMPORTED" in result.stdout + assert DEPRECATION_NOTICE not in result.stderr From 0e43d0f2b1fe19eeb781b99174c01601c771b026 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 11:25:28 +0200 Subject: [PATCH 3/8] feat(cli): show deprecation notice Co-Authored-By: Claude Opus 5.5 --- CLI_REFERENCE.md | 2 + docs/partials/get_started_cli.md | 2 + noxfile.py | 3 +- src/aignostics/cli.py | 64 +++++++++++++++++++++++++-- tests/aignostics/cli_test.py | 76 ++++++++++++++++++++++++++++++-- 5 files changed, 140 insertions(+), 7 deletions(-) diff --git a/CLI_REFERENCE.md b/CLI_REFERENCE.md index 1abb99636..1744cfed9 100644 --- a/CLI_REFERENCE.md +++ b/CLI_REFERENCE.md @@ -1,5 +1,7 @@ # CLI Reference +The Aignostics Python SDK is deprecated and gets only bug fixes and security fixes until April 30, 2027. After that date, the repository and the PyPI project are archived. Use the Aignostics Platform REST API (https://platform.aignostics.com/api/v1/docs) or the Console (https://platform.aignostics.com). + Command Line Interface (CLI) of Aignostics Python SDK providing access to Aignostics Platform. **Usage**: diff --git a/docs/partials/get_started_cli.md b/docs/partials/get_started_cli.md index e25945a20..a8b2742e7 100644 --- a/docs/partials/get_started_cli.md +++ b/docs/partials/get_started_cli.md @@ -56,6 +56,8 @@ uvx aignostics --help You should see the list of available command groups (`application`, `dataset`, `bucket`, `system`, and more). If the command isn't found, open a new terminal and try again. +Each command prints a deprecation notice to stderr. Stdout does not change, so scripts that read the output of a command continue to work. + ## Run Atlas H&E-TME from the command line This example downloads a public lung cancer dataset, submits an [Atlas H&E-TME](https://www.aignostics.com/products/he-tme-profiling-product) run, and downloads the results: diff --git a/noxfile.py b/noxfile.py index 5aa888ecd..79ab0e750 100644 --- a/noxfile.py +++ b/noxfile.py @@ -437,7 +437,7 @@ def _generate_openapi_schemas(session: nox.Session) -> None: f"--api-version={version}", *format_info["args"], ] - session.run(*cmd_args, stdout=f, external=True) + session.run(*cmd_args, stdout=f, stderr=None, external=True) session.log(f"Generated API {version} OpenAPI schema in {format_name} format") @@ -459,6 +459,7 @@ def _generate_sdk_metadata_schema(session: nox.Session, schema_type: str) -> Non "--env", LOG_STDERR_DISABLED, stdout=f, + stderr=None, external=True, ) diff --git a/src/aignostics/cli.py b/src/aignostics/cli.py index c5203d260..e4ce30616 100644 --- a/src/aignostics/cli.py +++ b/src/aignostics/cli.py @@ -2,12 +2,15 @@ import sys from importlib.util import find_spec +from itertools import pairwise from pathlib import Path +from typing import Any import typer from loguru import logger +from typer.core import TyperGroup -from aignostics.constants import NOTEBOOK_DEFAULT, WINDOW_TITLE +from aignostics.constants import DEPRECATION_NOTICE, NOTEBOOK_DEFAULT, WINDOW_TITLE from aignostics.utils import ( __is_running_in_container__, __python_version__, @@ -16,10 +19,63 @@ prepare_cli, ) +_JSON_OUTPUT_REQUESTED = "aignostics.json_output_requested" + + +def _requests_json_output(args: list[str]) -> bool: + """Check if the command arguments request JSON output. + + Args: + args (list[str]): Command line arguments. + + Returns: + bool: True if the arguments contain ``--format json`` or ``--format=json``. + """ + return "--format=json" in args or any(arg == "--format" and value == "json" for arg, value in pairwise(args)) + + +class _RootGroup(TyperGroup): + """Root command group that records if the command arguments request JSON output. + + Click empties ``ctx.args`` before it calls the root callback. So the group records the + decision in ``ctx.meta`` while it parses the arguments. + """ + + def parse_args(self, ctx: Any, args: list[str]) -> list[str]: # noqa: ANN401 + """Record if the arguments request JSON output, then parse them. + + Args: + ctx (Any): Click context of the root command group. + args (list[str]): Command line arguments. + + Returns: + list[str]: The remaining arguments. + """ + ctx.meta[_JSON_OUTPUT_REQUESTED] = _requests_json_output(args) + return super().parse_args(ctx, args) + + cli = typer.Typer( - help="Command Line Interface (CLI) of Aignostics Python SDK providing access to Aignostics Platform.", + cls=_RootGroup, + help=f"{DEPRECATION_NOTICE}\n\n" + "Command Line Interface (CLI) of Aignostics Python SDK providing access to Aignostics Platform.", ) + +def _print_deprecation_notice(ctx: typer.Context) -> None: + """Print the deprecation notice to stderr, so that stdout of each command does not change. + + Commands that write JSON (``--format json``) get no notice, because they can write a JSON error object + to stderr that consumers parse. + + Args: + ctx (typer.Context): Typer context of the root command. + """ + if ctx.meta.get(_JSON_OUTPUT_REQUESTED, False): + return + typer.secho(DEPRECATION_NOTICE, err=True, fg=typer.colors.YELLOW) + + if find_spec("nicegui") and find_spec("webview") and not __is_running_in_container__: @cli.command() @@ -120,7 +176,9 @@ def mcp_list_tools() -> None: cli.add_typer(mcp_cli) prepare_cli( - cli, f"🔬 Aignostics Python SDK v{__version__} - built with love in Berlin 🐻 // Python v{__python_version__}" + cli, + f"🔬 Aignostics Python SDK v{__version__} - built with love in Berlin 🐻 // Python v{__python_version__}", + root_callback=_print_deprecation_notice, ) diff --git a/tests/aignostics/cli_test.py b/tests/aignostics/cli_test.py index c1713e2c9..aa347e0e8 100644 --- a/tests/aignostics/cli_test.py +++ b/tests/aignostics/cli_test.py @@ -1,18 +1,19 @@ """Tests to verify the CLI functionality of OE Python Template.""" +import json import os import platform import subprocess import sys from importlib.util import find_spec -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from fastmcp import FastMCP -from typer.testing import CliRunner +from typer.testing import CliRunner, Result from aignostics.cli import cli -from aignostics.constants import WINDOW_TITLE +from aignostics.constants import DEPRECATION_NOTICE, WINDOW_TITLE from aignostics.utils import ( __python_version__, __version__, @@ -21,6 +22,8 @@ BUILT_WITH_LOVE = "built with love in Berlin" THE_VALUE = "THE_VALUE" +APPLICATION_CLI_CLIENT_PATCH_TARGET = "aignostics.application._cli.Client" +DOCUMENT_LIST_FAILURE_MESSAGE = "kaboom" @pytest.mark.integration @@ -54,6 +57,73 @@ def test_cli_python_version_format(runner) -> None: assert pattern not in normalized_output, f"Epilog should not contain '{pattern}'" +@pytest.mark.integration +def test_cli_help_starts_with_deprecation_notice(runner: CliRunner) -> None: + """Check that the root help text shows the deprecation notice before the CLI description.""" + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + + # Rich wraps the help text, so collapse all whitespace before the assertion + normalized_output = " ".join(normalize_output(result.output).split()) + notice_position = normalized_output.find(DEPRECATION_NOTICE) + assert notice_position != -1 + assert notice_position < normalized_output.find("Command Line Interface") + + +@pytest.mark.integration +def test_cli_command_prints_deprecation_notice_to_stderr(runner: CliRunner) -> None: + """Check that a command prints the deprecation notice to stderr and keeps stdout free of it.""" + with patch(PATCH_MCP_LOCATE_IMPLEMENTATIONS, return_value=[]): + result = runner.invoke(cli, ["mcp", "list-tools"]) + assert result.exit_code == 0 + assert DEPRECATION_NOTICE in normalize_output(result.stderr) + assert DEPRECATION_NOTICE not in normalize_output(result.stdout) + + +@pytest.mark.integration +def test_cli_subcommand_help_prints_deprecation_notice(runner: CliRunner) -> None: + """Check that help of a subcommand also prints the deprecation notice to stderr.""" + result = runner.invoke(cli, ["application", "--help"]) + assert result.exit_code == 0 + assert DEPRECATION_NOTICE in normalize_output(result.stderr) + + +def _invoke_failing_document_list(runner: CliRunner, format_args: list[str]) -> Result: + """Invoke `application version document list` offline, with a client that fails to list documents. + + Args: + runner (CliRunner): CLI test runner. + format_args (list[str]): Arguments that select the output format. + + Returns: + Result: Result of the CLI invocation. + """ + fake_documents = MagicMock() + fake_documents.list.side_effect = RuntimeError(DOCUMENT_LIST_FAILURE_MESSAGE) + fake_client = MagicMock() + fake_client.applications.versions.documents.return_value = fake_documents + with patch(APPLICATION_CLI_CLIENT_PATCH_TARGET, return_value=fake_client): + return runner.invoke(cli, ["application", "version", "document", "list", "heta", *format_args]) + + +@pytest.mark.integration +@pytest.mark.parametrize("format_args", [["--format", "json"], ["--format=json"]]) +def test_cli_json_format_skips_deprecation_notice(runner: CliRunner, format_args: list[str]) -> None: + """Check that a command with JSON output prints no deprecation notice, so stderr stays parseable JSON.""" + result = _invoke_failing_document_list(runner, format_args) + assert result.exit_code == 1 + assert DEPRECATION_NOTICE not in normalize_output(result.stderr) + assert json.loads(result.stderr)["message"] == DOCUMENT_LIST_FAILURE_MESSAGE + + +@pytest.mark.integration +def test_cli_text_format_prints_deprecation_notice(runner: CliRunner) -> None: + """Check that the same command with text output prints the deprecation notice to stderr.""" + result = _invoke_failing_document_list(runner, ["--format", "text"]) + assert result.exit_code == 1 + assert DEPRECATION_NOTICE in normalize_output(result.stderr) + + @pytest.mark.integration @pytest.mark.timeout(timeout=60) def test_cli_fails_on_invalid_setting_with_env_arg() -> None: From 4157fdcd391c14d24a1aa550b68ea0f6db33e4ae Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 11:31:36 +0200 Subject: [PATCH 4/8] feat(gui): show deprecation banner in Launchpad Co-Authored-By: Claude Opus 5.5 --- docs/partials/get_started_launchpad.md | 2 ++ src/aignostics/gui/CLAUDE.md | 8 ++++++ src/aignostics/gui/_frame.py | 13 ++++++++- tests/aignostics/gui/__init__.py | 1 + tests/aignostics/gui/frame_test.py | 40 ++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/aignostics/gui/__init__.py create mode 100644 tests/aignostics/gui/frame_test.py diff --git a/docs/partials/get_started_launchpad.md b/docs/partials/get_started_launchpad.md index 9a6281a31..21b2cc600 100644 --- a/docs/partials/get_started_launchpad.md +++ b/docs/partials/get_started_launchpad.md @@ -49,6 +49,8 @@ uvx aignostics launchpad The first time you run this, it takes a minute or two to get ready. Then the **Launchpad window opens** — that means everything installed correctly. +Each page of the window shows a deprecation banner below the header. The banner has links to the alternatives: the Aignostics Platform REST API docs and the Console. + ![The Launchpad window when it first opens](_static/launchpad/01-launchpad-window.png) If you see an error instead of the window, see [Troubleshooting](#troubleshooting). diff --git a/src/aignostics/gui/CLAUDE.md b/src/aignostics/gui/CLAUDE.md index 39cf5b531..fa0bc6657 100644 --- a/src/aignostics/gui/CLAUDE.md +++ b/src/aignostics/gui/CLAUDE.md @@ -19,6 +19,14 @@ The gui module provides common GUI framework components and theming for the Aign - `_frame.py` - Common layout components and health updates - `_error.py` - Error page handling (in-file class `PageBuilder`, imported as `ErrorPageBuilder`) +**Deprecation Banner (`_frame.py`):** + +- `frame()` renders a full-width banner directly below the header, before the page content (and the left drawer), so every page that uses `frame()` shows it +- Content: a `warning` icon, `aignostics.constants.DEPRECATION_NOTICE` as a label, and two links that open in a new tab: "Open REST API docs" (`PLATFORM_API_DOCS_URL`) and "Open Console" (`CONSOLE_URL`) +- Marker: `DEPRECATION_BANNER` (used by `tests/aignostics/gui/frame_test.py`) +- The banner is not closable: it has no close button and no state to hide it +- Do not render the notice with `ui.html`; use `ui.label` and `ui.link` + **Usage Pattern:** - Provides shared `PageBuilder` class for module auto-discovery diff --git a/src/aignostics/gui/_frame.py b/src/aignostics/gui/_frame.py index 0bbf83222..c868bcd30 100644 --- a/src/aignostics/gui/_frame.py +++ b/src/aignostics/gui/_frame.py @@ -14,7 +14,7 @@ from humanize import naturaldelta from loguru import logger -from aignostics.constants import CONSOLE_URL, WINDOW_TITLE +from aignostics.constants import CONSOLE_URL, DEPRECATION_NOTICE, PLATFORM_API_DOCS_URL, WINDOW_TITLE from aignostics.utils import __version__, open_user_data_directory from ._theme import theme @@ -28,6 +28,7 @@ CLASSES_FULL_WIDTH = "w-full" CLASSES_FULL_HEIGHT = "h-full" CLASSES_FULL_SIZE = f"{CLASSES_FULL_WIDTH} {CLASSES_FULL_HEIGHT}" +CLASSES_BANNER_LINK = "text-black underline whitespace-nowrap" @contextmanager @@ -272,6 +273,16 @@ def toggle_dark_mode() -> None: with ui.button(on_click=lambda _: right_drawer.toggle(), icon="menu").props(FLAT_COLOR_WHITE): ui.tooltip("Open menu") + with ( + ui.row() + .classes(f"{CLASSES_FULL_WIDTH} items-center no-wrap gap-3 px-4 py-2 bg-amber-100 text-black") + .mark("DEPRECATION_BANNER") + ): + ui.icon("warning", color="warning").classes("text-2xl") + ui.label(DEPRECATION_NOTICE).classes("grow") + ui.link("Open REST API docs", PLATFORM_API_DOCS_URL, new_tab=True).classes(CLASSES_BANNER_LINK) + ui.link("Open Console", CONSOLE_URL, new_tab=True).classes(CLASSES_BANNER_LINK) + if left_sidebar: with ui.left_drawer(top_corner=True, bottom_corner=True, elevated=True).props("breakpoint=0"): yield diff --git a/tests/aignostics/gui/__init__.py b/tests/aignostics/gui/__init__.py new file mode 100644 index 000000000..d626240e2 --- /dev/null +++ b/tests/aignostics/gui/__init__.py @@ -0,0 +1 @@ +"""Tests of gui module.""" diff --git a/tests/aignostics/gui/frame_test.py b/tests/aignostics/gui/frame_test.py new file mode 100644 index 000000000..ca989f223 --- /dev/null +++ b/tests/aignostics/gui/frame_test.py @@ -0,0 +1,40 @@ +"""Tests of the shared Launchpad page frame.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from nicegui import ui + +from aignostics.constants import CONSOLE_URL, DEPRECATION_NOTICE, PLATFORM_API_DOCS_URL + +if TYPE_CHECKING: + from nicegui.testing import User + + +@pytest.mark.integration +async def test_gui_deprecation_banner_on_start_page(user: User, silent_logging: None) -> None: + """The start page shows the deprecation banner with the notice text.""" + await user.open("/") + await user.should_see(marker="DEPRECATION_BANNER") + await user.should_see(DEPRECATION_NOTICE) + + +@pytest.mark.integration +async def test_gui_deprecation_banner_on_system_page(user: User, silent_logging: None) -> None: + """The system page shows the same banner, so the banner is part of the shared frame.""" + await user.open("/system") + await user.should_see(marker="DEPRECATION_BANNER") + await user.should_see(DEPRECATION_NOTICE) + + +@pytest.mark.integration +async def test_gui_deprecation_banner_links_to_alternatives(user: User, silent_logging: None) -> None: + """The banner itself links to the REST API docs and the Console (the drawer link does not count).""" + await user.open("/") + await user.should_see(marker="DEPRECATION_BANNER") + banner = user.find(marker="DEPRECATION_BANNER").elements.pop() + link_targets = {link.props.get("href") for link in user.find(ui.link).elements if link in banner.descendants()} + assert PLATFORM_API_DOCS_URL in link_targets + assert CONSOLE_URL in link_targets From 5d39a104d0675da345ea6f606f944868e3065c95 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 11:33:23 +0200 Subject: [PATCH 5/8] docs: add deprecation notice to README Co-Authored-By: Claude Opus 5.5 --- README.md | 13 +++++++++++++ docs/partials/README_header.md | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/README.md b/README.md index b20726ffc..6e3daee8b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,19 @@ # 🔬Aignostics Python SDK +## ⚠️ Deprecated + +> The Aignostics Python SDK is deprecated and gets only bug fixes and security fixes until April 30, 2027. After that date, the repository and the PyPI project are archived. Use the Aignostics Platform REST API (https://platform.aignostics.com/api/v1/docs) or the Console (https://platform.aignostics.com). + +### Migration + +| You use | Use this instead | +| --- | --- | +| Python Library | Aignostics Platform REST API: [https://platform.aignostics.com/api/v1/docs](https://platform.aignostics.com/api/v1/docs) | +| CLI | Aignostics Platform REST API: [https://platform.aignostics.com/api/v1/docs](https://platform.aignostics.com/api/v1/docs) | +| Launchpad | Console: [https://platform.aignostics.com](https://platform.aignostics.com) | +| SDK upload command for the Console | Console: [https://platform.aignostics.com](https://platform.aignostics.com) | + [![License](https://img.shields.io/github/license/aignostics/python-sdk?logo=opensourceinitiative&logoColor=3DA639&labelColor=414042&color=A41831)](https://github.com/aignostics/python-sdk/blob/main/LICENSE) [![Python Version](https://img.shields.io/pypi/pyversions/aignostics.svg?logo=python&color=204361&labelColor=1E2933)](https://pypi.org/project/aignostics/) [![CI/CD](https://github.com/aignostics/python-sdk/actions/workflows/ci-cd.yml/badge.svg?branch=main)](https://github.com/aignostics/python-sdk/actions/workflows/ci-cd.yml) diff --git a/docs/partials/README_header.md b/docs/partials/README_header.md index 767a696ea..7d6b2d8dd 100644 --- a/docs/partials/README_header.md +++ b/docs/partials/README_header.md @@ -1,5 +1,18 @@ # 🔬Aignostics Python SDK +## ⚠️ Deprecated + +> The Aignostics Python SDK is deprecated and gets only bug fixes and security fixes until April 30, 2027. After that date, the repository and the PyPI project are archived. Use the Aignostics Platform REST API (https://platform.aignostics.com/api/v1/docs) or the Console (https://platform.aignostics.com). + +### Migration + +| You use | Use this instead | +| --- | --- | +| Python Library | Aignostics Platform REST API: [https://platform.aignostics.com/api/v1/docs](https://platform.aignostics.com/api/v1/docs) | +| CLI | Aignostics Platform REST API: [https://platform.aignostics.com/api/v1/docs](https://platform.aignostics.com/api/v1/docs) | +| Launchpad | Console: [https://platform.aignostics.com](https://platform.aignostics.com) | +| SDK upload command for the Console | Console: [https://platform.aignostics.com](https://platform.aignostics.com) | + [![License](https://img.shields.io/github/license/aignostics/python-sdk?logo=opensourceinitiative&logoColor=3DA639&labelColor=414042&color=A41831)](https://github.com/aignostics/python-sdk/blob/main/LICENSE) [![Python Version](https://img.shields.io/pypi/pyversions/aignostics.svg?logo=python&color=204361&labelColor=1E2933)](https://pypi.org/project/aignostics/) [![CI/CD](https://github.com/aignostics/python-sdk/actions/workflows/ci-cd.yml/badge.svg?branch=main)](https://github.com/aignostics/python-sdk/actions/workflows/ci-cd.yml) From be56705d3459f61e158ab68e9ec07ce0ec2ad4c9 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 11:35:46 +0200 Subject: [PATCH 6/8] docs: add deprecation banner to docs site Co-Authored-By: Claude Opus 5.5 --- docs/source/_static/custom.css | 10 ++++++++++ docs/source/conf.py | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index ff9de28a5..348afd7a6 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -34,3 +34,13 @@ width: 100% !important; height: auto !important; } + +/* Deprecation announcement: wrap the long notice so that both links stay + visible. Furo's default is one line with a fixed height and a horizontal scroll. */ +.announcement { + height: auto; + min-height: var(--header-height); +} +.announcement-content { + white-space: normal; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index 19d45a8cc..f072b4d31 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,10 +1,13 @@ """Sphinx configuration.""" +import html import re import sys from datetime import UTC, datetime from pathlib import Path +from aignostics.constants import DEPRECATION_NOTICE + # Add _ext directory to sys.path for custom extensions sys.path.insert(0, str(Path(__file__).parent / "_ext")) @@ -98,6 +101,14 @@ "api_reference_v1.rst", ] +# Deprecation banner on each page: the notice with each URL as a clickable link. +# The closing parenthesis after a URL in the notice is not part of the link. +DEPRECATION_ANNOUNCEMENT = re.sub( + r"https://[^\s)]+", + lambda match: f'{match.group(0)}', + html.escape(DEPRECATION_NOTICE), +) + html_theme = "furo" html_static_path = ["_static"] html_css_files = ["custom.css"] @@ -120,6 +131,7 @@ "color-brand-primary": "#9b95ec", "color-brand-content": "#9b95ec", }, + "announcement": DEPRECATION_ANNOUNCEMENT, } From 37337a5b645feb3ad0bdb0fab2415c0ba61cbac5 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Wed, 7 Oct 2026 11:37:00 +0200 Subject: [PATCH 7/8] docs: state maintenance-only policy Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 14 ++++++++++++++ CONTRIBUTING.md | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 6dd9da5b8..df3d8557f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,20 @@ If you you are creating a pull request yourself: * Add a label skip:test:long_running, to skip running long running tests. This is important because some tests in this repository are marked as long_running and can take a significant amount of time to complete. By adding this label, you help ensure that the CI pipeline runs efficiently and avoids unnecessary delays. +## Maintenance mode + +The Aignostics Python SDK is deprecated. Support ends on April 30, 2027. +Until that date, the SDK is in maintenance mode. + +Use these rules for each change: + +1. Accept anomaly fixes through PR-SOP-01. +2. Accept dependency updates that fix vulnerabilities. +3. Reject new features. +4. Keep the deprecation warning in each release. + +For the notice text and the surfaces that show it, refer to [Deprecation notice](#deprecation-notice). + ## Module Documentation Index Every module has detailed CLAUDE.md documentation. For module-specific guidance, see: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e42cd143f..667a012fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,18 @@ Thank you for considering contributing to Aignostics Python SDK! +## Maintenance mode + +The Aignostics Python SDK is deprecated. Support ends on April 30, 2027. +Until that date, the SDK is in maintenance mode. + +Use these rules for each change: + +1. Accept anomaly fixes through PR-SOP-01. +2. Accept dependency updates that fix vulnerabilities. +3. Reject new features. +4. Keep the deprecation warning in each release. + ## Setup [Create a fork](https://github.com/aignostics/python-sdk/fork) and clone your fork using ```git clone URL_OF_YOUR_CLONE```. Then change into the directory of your local Aignostics Python SDK repository with ```cd python-sdk```. From f87508bbc01346676433a485e1646be6fb2621c3 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Mon, 12 Oct 2026 12:38:46 +0200 Subject: [PATCH 8/8] test(system): compare stdout in proxy config test The CLI now writes a notice to stderr, and CliRunner's result.output mixes stdout and stderr. Compare only stdout for the empty config values. Co-Authored-By: Claude Opus 5.5 --- tests/aignostics/system/cli_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/aignostics/system/cli_test.py b/tests/aignostics/system/cli_test.py index faeff9bc5..9030bb577 100644 --- a/tests/aignostics/system/cli_test.py +++ b/tests/aignostics/system/cli_test.py @@ -396,15 +396,15 @@ def test_cli_http_proxy(runner: CliRunner, silent_logging, tmp_path: Path, recor result = runner.invoke(cli, ["system", "config", "get", "SSL_CERT_FILE"]) assert result.exit_code == 0 - assert result.output == "\n" + assert result.stdout == "\n" result = runner.invoke(cli, ["system", "config", "get", "REQUESTS_CA_BUNDLE"]) assert result.exit_code == 0 - assert result.output == "\n" + assert result.stdout == "\n" result = runner.invoke(cli, ["system", "config", "get", "CURL_CA_BUNDLE"]) assert result.exit_code == 0 - assert result.output == "\n" + assert result.stdout == "\n" # Enable with no verify and ssl cert file conclicts