This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-releases-client.git


The following commit(s) were added to refs/heads/main by this push:
     new d0070b3  Make outputs compatible with testing, and fix a configuration 
issue
d0070b3 is described below

commit d0070b386ab76712a6f755b460a5ed9c5a794f27
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Jul 10 16:49:25 2025 +0100

    Make outputs compatible with testing, and fix a configuration issue
---
 pyproject.toml          |  4 +-
 src/atrclient/client.py | 99 ++++++++++++++++++++++---------------------------
 tests/cli_config.t      | 12 +++++-
 tests/cli_version.t     |  2 +-
 tests/test_all.py       | 13 +++++--
 uv.lock                 |  4 +-
 6 files changed, 70 insertions(+), 64 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 2100e59..59a191f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "hatchling.build"
 
 [project]
 name            = "apache-trusted-releases"
-version         = "0.20250710.1457"
+version         = "0.20250710.1549"
 description     = "ATR CLI and Python API"
 readme          = "README.md"
 requires-python = ">=3.13"
@@ -48,4 +48,4 @@ atr = "atrclient.client:main"
 packages = ["src/atrclient"]
 
 [tool.uv]
-exclude-newer = "2025-07-10T14:57:00Z"
+exclude-newer = "2025-07-10T15:49:00Z"
diff --git a/src/atrclient/client.py b/src/atrclient/client.py
index ec74e77..474fab8 100755
--- a/src/atrclient/client.py
+++ b/src/atrclient/client.py
@@ -23,17 +23,17 @@ from __future__ import annotations
 import asyncio
 import base64
 import contextlib
+import copy
 import datetime
 import importlib.metadata as metadata
 import json
-import logging
 import os
 import pathlib
 import re
 import signal
 import sys
 import time
-from typing import TYPE_CHECKING, Annotated, Any, Literal
+from typing import TYPE_CHECKING, Annotated, Any, Literal, NoReturn
 
 import aiohttp
 import cyclopts
@@ -50,7 +50,6 @@ CHECKS: cyclopts.App = cyclopts.App(name="checks", 
help="Check result operations
 CONFIG: cyclopts.App = cyclopts.App(name="config", help="Configuration 
operations.")
 DEV: cyclopts.App = cyclopts.App(name="dev", help="Developer operations.")
 JWT: cyclopts.App = cyclopts.App(name="jwt", help="JWT operations.")
-LOGGER = logging.getLogger(__name__)
 RELEASE: cyclopts.App = cyclopts.App(name="release", help="Release 
operations.")
 VERSION: str = metadata.version("apache-trusted-releases")
 VOTE: cyclopts.App = cyclopts.App(name="vote", help="Vote operations.")
@@ -124,8 +123,7 @@ def app_checks_status(
             break
 
     if target_release is None:
-        LOGGER.error(f"Release {project}-{version} not found.")
-        sys.exit(1)
+        show_error_and_exit(f"Release {project}-{version} not found.")
 
     phase = target_release.get("phase")
     if phase != "release_candidate_draft":
@@ -158,8 +156,7 @@ def app_checks_warnings(
 def app_config_file() -> None:
     path = config_path()
     if not path.exists():
-        LOGGER.error("No configuration file found.")
-        sys.exit(1)
+        show_error_and_exit("No configuration file found.")
 
     with path.open("r", encoding="utf-8") as fh:
         for chunk in fh:
@@ -186,8 +183,7 @@ def app_dev_env() -> None:
 def app_dev_stamp() -> None:
     path = pathlib.Path("pyproject.toml")
     if not path.exists():
-        LOGGER.error("pyproject.toml not found.")
-        sys.exit(1)
+        show_error_and_exit("pyproject.toml not found.")
 
     text_v1 = path.read_text()
 
@@ -201,23 +197,23 @@ def app_dev_stamp() -> None:
 
     if version_updated or exclude_newer_updated:
         path.write_text(text_v3, "utf-8")
-    LOGGER.info(
+    print(
         "Updated exclude-newer."
         if exclude_newer_updated
         else "Did not update exclude-newer."
     )
-    LOGGER.info("Updated version." if version_updated else "Did not update 
version.")
+    print("Updated version." if version_updated else "Did not update version.")
 
     path = pathlib.Path("tests/cli_version.t")
     if not path.exists():
-        LOGGER.warning("tests/cli_version.t not found.")
+        show_warning("tests/cli_version.t not found.")
         return
     text_v1 = path.read_text(encoding="utf-8")
     text_v2 = re.sub(r"0\.\d{8}\.\d{4}", v, text_v1)
     version_updated = not (text_v1 == text_v2)
     if version_updated:
         path.write_text(text_v2, "utf-8")
-        LOGGER.info("Updated tests/cli_version.t.")
+        print("Updated tests/cli_version.t.")
 
 
 @APP.command(name="docs", help="Show comprehensive CLI documentation in 
Markdown.")
@@ -233,16 +229,14 @@ def app_docs() -> None:
 def app_drop(path: str) -> None:
     parts = path.split(".")
     if not parts:
-        LOGGER.error("Not a valid configuration key")
-        sys.exit(1)
+        show_error_and_exit("Not a valid configuration key")
 
     with config_lock(write=True) as config:
         present, _ = config_walk(config, parts, "drop")
         if not present:
-            LOGGER.error(f"Could not find {path} in the configuration file")
-            sys.exit(1)
+            show_error_and_exit(f"Could not find {path} in the configuration 
file")
 
-    LOGGER.info(f"Removed {path}.")
+    print(f"Removed {path}.")
 
 
 @JWT.command(name="dump", help="Show decoded JWT payload from stored config.")
@@ -251,14 +245,12 @@ def app_jwt_dump() -> None:
 
     header = jwt.get_unverified_header(jwt_value)
     if header != {"alg": "HS256", "typ": "JWT"}:
-        LOGGER.error("Invalid JWT header.")
-        sys.exit(1)
+        show_error_and_exit("Invalid JWT header.")
 
     try:
         payload = jwt.decode(jwt_value, options={"verify_signature": False})
     except jwt.PyJWTError as e:
-        LOGGER.error(f"Failed to decode JWT: {e}")
-        sys.exit(1)
+        show_error_and_exit(f"Failed to decode JWT: {e}")
 
     print(json.dumps(payload, indent=None))
 
@@ -341,28 +333,25 @@ def app_revisions(project: str, version: str) -> None:
 def app_set(path: str, value: str) -> None:
     parts = path.split(".")
     if not parts:
-        LOGGER.error("Not a valid configuration key.")
-        sys.exit(1)
+        show_error_and_exit("Not a valid configuration key.")
 
     with config_lock(write=True) as config:
         config_set(config, path.split("."), value)
 
-    LOGGER.info(f"Set {path} to {value}.")
+    print(f"Set {path} to {json.dumps(value, indent=None)}.")
 
 
 @APP.command(name="show", help="Show a configuration value using dot 
notation.")
 def app_show(path: str) -> None:
     parts = path.split(".")
     if not parts:
-        LOGGER.error("Not a valid configuration key.")
-        sys.exit(1)
+        show_error_and_exit("Not a valid configuration key.")
 
     with config_lock() as config:
         value = config_get(config, parts)
 
     if value is None:
-        LOGGER.error(f"Could not find {path} in the configuration file.")
-        sys.exit(1)
+        show_error_and_exit(f"Could not find {path} in the configuration 
file.")
 
     print(value)
 
@@ -522,8 +511,7 @@ def config_jwt_get() -> str:
         jwt_value = config_get(config, ["tokens", "jwt"])
 
     if jwt_value is None:
-        LOGGER.error("No JWT stored in configuration.")
-        sys.exit(1)
+        show_error_and_exit("No JWT stored in configuration.")
 
     return jwt_value
 
@@ -537,11 +525,9 @@ def config_jwt_payload() -> tuple[str, dict[str, Any]]:
     try:
         payload = jwt.decode(jwt_value, options={"verify_signature": False})
     except jwt.PyJWTError as e:
-        LOGGER.error(f"Failed to decode JWT: {e}")
-        sys.exit(1)
+        show_error_and_exit(f"Failed to decode JWT: {e}")
     if not isinstance(payload, dict):
-        LOGGER.error("Invalid JWT payload.")
-        sys.exit(1)
+        show_error_and_exit("Invalid JWT payload.")
     return jwt_value, payload
 
 
@@ -550,8 +536,7 @@ def config_jwt_refresh(asf_uid: str | None = None) -> str:
         pat_value = config_get(config, ["tokens", "pat"])
 
     if pat_value is None:
-        LOGGER.error("No Personal Access Token stored.")
-        sys.exit(1)
+        show_error_and_exit("No Personal Access Token stored.")
 
     host, verify_ssl = config_host_get()
     url = f"https://{host}/api/jwt";
@@ -560,8 +545,7 @@ def config_jwt_refresh(asf_uid: str | None = None) -> str:
         asf_uid = config.get("asf", {}).get("uid")
 
     if asf_uid is None:
-        LOGGER.error("No ASF UID provided and asf.uid not configured.")
-        sys.exit(1)
+        show_error_and_exit("No ASF UID provided and asf.uid not configured.")
 
     jwt_token = asyncio.run(web_fetch(url, asf_uid, pat_value, verify_ssl))
 
@@ -605,7 +589,7 @@ def config_read() -> dict[str, Any]:
             return data
         except strictyaml.YAMLValidationError as e:
             raise RuntimeError(f"Invalid atr.yaml: {e}") from e
-    return YAML_DEFAULTS.copy()
+    return copy.deepcopy(YAML_DEFAULTS)
 
 
 def config_set(config: dict[str, Any], parts: list[str], val: Any) -> None:
@@ -709,7 +693,6 @@ def initialise() -> None:
     # We do this because pytest_console_scripts.ScriptRunner invokes main 
multiple times
     APP.version = VERSION
     signal.signal(signal.SIGPIPE, signal.SIG_DFL)
-    logging.basicConfig(level=logging.INFO, format="%(message)s")
     subcommands_register(APP)
 
 
@@ -735,8 +718,7 @@ def main() -> None:
 
 def releases_display(result: dict[str, Any]) -> None:
     if ("data" not in result) or ("count" not in result):
-        LOGGER.error("Invalid response format")
-        sys.exit(1)
+        show_error_and_exit("Invalid response format")
 
     releases = result["data"]
     count = result["count"]
@@ -762,6 +744,17 @@ def releases_display(result: dict[str, Any]) -> None:
         print(f"  {version:<24} {latest:<7} {phase_short:<11} 
{created_formatted}")
 
 
+def show_error_and_exit(message: str, code: int = 1) -> NoReturn:
+    sys.stderr.write(f"atr: error: {message}\n")
+    sys.stderr.flush()
+    raise SystemExit(code)
+
+
+def show_warning(message: str) -> None:
+    sys.stderr.write(f"atr: warning: {message}\n")
+    sys.stderr.flush()
+
+
 def subcommands_register(app: cyclopts.App) -> None:
     app.command(CHECKS)
     app.command(CONFIG)
@@ -793,8 +786,7 @@ async def web_fetch(
         async with session.post(url, json=payload) as resp:
             if resp.status != 200:
                 text = await resp.text()
-                LOGGER.error(f"JWT fetch failed: {resp.status} {text}")
-                sys.exit(1)
+                show_error_and_exit(f"JWT fetch failed: {resp.status} {text}")
 
             data: dict[str, Any] = await resp.json()
             if "jwt" in data:
@@ -812,12 +804,11 @@ async def web_get(url: str, jwt_token: str, verify_ssl: 
bool = True) -> Any:
                 try:
                     error_data = json.loads(text)
                     if isinstance(error_data, dict) and "error" in error_data:
-                        LOGGER.error(error_data["error"])
+                        show_error_and_exit(error_data["error"])
                     else:
-                        LOGGER.error(f"Request failed: {resp.status} {text}")
+                        show_error_and_exit(f"Request failed: {resp.status} 
{text}")
                 except json.JSONDecodeError:
-                    LOGGER.error(f"Request failed: {resp.status} {text}")
-                sys.exit(1)
+                    show_error_and_exit(f"Request failed: {resp.status} 
{text}")
             return await resp.json()
 
 
@@ -830,12 +821,11 @@ async def web_get_public(url: str, verify_ssl: bool = 
True) -> Any:
                 try:
                     error_data = json.loads(text)
                     if isinstance(error_data, dict) and "error" in error_data:
-                        LOGGER.error(error_data["error"])
+                        show_error_and_exit(error_data["error"])
                     else:
-                        LOGGER.error(f"Request failed: {resp.status} {text}")
+                        show_error_and_exit(f"Request failed: {resp.status} 
{text}")
                 except json.JSONDecodeError:
-                    LOGGER.error(f"Request failed: {resp.status} {text}")
-                sys.exit(1)
+                    show_error_and_exit(f"Request failed: {resp.status} 
{text}")
             return await resp.json()
 
 
@@ -848,8 +838,7 @@ async def web_post(
         async with session.post(url, json=payload) as resp:
             if resp.status not in (200, 201):
                 text = await resp.text()
-                LOGGER.error(f"Release add failed: {resp.status} {text}")
-                sys.exit(1)
+                show_error_and_exit(f"Release add failed: {resp.status} 
{text}")
 
             try:
                 return await resp.json()
diff --git a/tests/cli_config.t b/tests/cli_config.t
index b24bb31..794d7c0 100644
--- a/tests/cli_config.t
+++ b/tests/cli_config.t
@@ -4,4 +4,14 @@ ATR_CLIENT_CONFIG_PATH="<!CONFIG_PATH!>"
 
 ! atr config file
 <!stderr!>
-No configuration file found.
+atr: error: No configuration file found.
+
+$ atr config path
+/<!ROOT_REL_PATH!>
+
+$ atr set asf.uid example
+Set asf.uid to "example".
+
+$ atr config file
+asf:
+  uid: example
diff --git a/tests/cli_version.t b/tests/cli_version.t
index dfa16d1..cdb7824 100644
--- a/tests/cli_version.t
+++ b/tests/cli_version.t
@@ -1,2 +1,2 @@
 $ atr --version
-0.20250710.1457
+0.20250710.1549
diff --git a/tests/test_all.py b/tests/test_all.py
index e63db3b..b9af31e 100755
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -181,7 +181,10 @@ def test_app_set_show(
 ) -> None:
     client.app_set("atr.host", "example.invalid")
     client.app_show("atr.host")
-    assert capsys.readouterr().out == "example.invalid\n"
+    assert (
+        capsys.readouterr().out
+        == 'Set atr.host to "example.invalid".\nexample.invalid\n'
+    )
 
 
 def test_cli_version(script_runner: pytest_console_scripts.ScriptRunner) -> 
None:
@@ -200,6 +203,12 @@ def test_cli_transcripts(
     fixture_config_env: pathlib.Path,
 ) -> None:
     r_variable = re.compile(r"<!([A-Z_]+)!>")
+    env = os.environ.copy()
+    transcript_config_path = fixture_config_env
+    # transcript_config_path = 
fixture_config_env.with_suffix(f".{transcript_path.name}")
+    # if transcript_config_path.exists():
+    #     pytest.fail(f"Transcript config file already exists: 
{transcript_config_path}")
+    env["ATR_CLIENT_CONFIG_PATH"] = str(transcript_config_path)
     with open(transcript_path, "r", encoding="utf-8") as f:
         actual_output = []
         for line in f:
@@ -210,8 +219,6 @@ def test_cli_transcripts(
                 if not command.startswith("atr"):
                     pytest.fail(f"Command does not start with 'atr': 
{command}")
                     return
-                env = os.environ.copy()
-                env["ATR_CLIENT_CONFIG_PATH"] = str(fixture_config_env)
                 result = script_runner.run(shlex.split(command), env=env)
                 assert result.returncode == expected_code
                 actual_output[:] = result.stdout.splitlines()
diff --git a/uv.lock b/uv.lock
index 5fd67ab..b00b6b0 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,7 @@ version = 1
 requires-python = ">=3.13"
 
 [options]
-exclude-newer = "2025-07-10T14:57:00Z"
+exclude-newer = "2025-07-10T15:49:00Z"
 
 [[package]]
 name = "aiohappyeyeballs"
@@ -74,7 +74,7 @@ wheels = [
 
 [[package]]
 name = "apache-trusted-releases"
-version = "0.20250710.1457"
+version = "0.20250710.1549"
 source = { editable = "." }
 dependencies = [
     { name = "aiohttp" },


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to