openapi-cli4ai 0.5.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.github/dependabot.yml +1 -1
  2. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.github/workflows/ci.yml +8 -8
  3. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.github/workflows/codeql.yml +3 -3
  4. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.github/workflows/publish.yml +3 -3
  5. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/CHANGELOG.md +12 -0
  6. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/PKG-INFO +1 -1
  7. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/pyproject.toml +3 -2
  8. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/src/openapi_cli4ai/cli.py +19 -4
  9. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_auth.py +1 -1
  10. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_auth_flow_security.py +2 -2
  11. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_auth_flows.py +1 -1
  12. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_caching.py +78 -0
  13. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_commands.py +6 -0
  14. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_coverage_gaps.py +1 -1
  15. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_device_flow.py +1 -1
  16. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_error_handling.py +3 -3
  17. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_oidc.py +1 -1
  18. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_security.py +7 -7
  19. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_v040_hardening.py +3 -3
  20. openapi_cli4ai-0.5.1/uv.lock +704 -0
  21. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.github/CODEOWNERS +0 -0
  22. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  23. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  24. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.github/release.yml +0 -0
  25. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.gitignore +0 -0
  26. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/.pre-commit-config.yaml +0 -0
  27. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/CONTRIBUTING.md +0 -0
  28. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/LICENSE +0 -0
  29. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/README.md +0 -0
  30. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/SECURITY.md +0 -0
  31. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/demo.gif +0 -0
  32. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/examples/profiles.toml.example +0 -0
  33. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/openapi-cli4ai +0 -0
  34. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/src/openapi_cli4ai/__init__.py +0 -0
  35. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/src/openapi_cli4ai/__main__.py +0 -0
  36. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/src/openapi_cli4ai/py.typed +0 -0
  37. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/__init__.py +0 -0
  38. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/conftest.py +0 -0
  39. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/fixtures/petstore_spec.json +0 -0
  40. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_cli_commands.py +0 -0
  41. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_features.py +0 -0
  42. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_foundation.py +0 -0
  43. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_integration.py +0 -0
  44. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_profile_management.py +0 -0
  45. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_run_command.py +0 -0
  46. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_schema_composition.py +0 -0
  47. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_spec_parsing.py +0 -0
  48. {openapi_cli4ai-0.5.0 → openapi_cli4ai-0.5.1}/tests/test_sse_streaming.py +0 -0
@@ -10,7 +10,7 @@ updates:
10
10
  actions:
11
11
  patterns: ["*"]
12
12
 
13
- - package-ecosystem: "pip"
13
+ - package-ecosystem: "uv"
14
14
  directory: "/"
15
15
  schedule:
16
16
  interval: "weekly"
@@ -16,22 +16,22 @@ jobs:
16
16
  matrix:
17
17
  python-version: ["3.11", "3.12", "3.13"]
18
18
  steps:
19
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
20
- - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
19
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
20
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
21
21
  with:
22
22
  enable-cache: true
23
23
  - name: Set up Python ${{ matrix.python-version }}
24
24
  run: uv python install ${{ matrix.python-version }}
25
25
  - name: Sync dependencies
26
- run: uv sync --python ${{ matrix.python-version }}
26
+ run: uv sync --locked --python ${{ matrix.python-version }}
27
27
  - name: Run unit tests with coverage
28
28
  run: uv run --python ${{ matrix.python-version }} pytest tests/ -m "not integration" -v --cov=openapi_cli4ai --cov-report=term-missing
29
29
 
30
30
  lint:
31
31
  runs-on: ubuntu-latest
32
32
  steps:
33
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
34
- - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
33
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
34
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
35
35
  - name: Ruff check
36
36
  run: uvx ruff check .
37
37
  - name: Ruff format check
@@ -40,9 +40,9 @@ jobs:
40
40
  typecheck:
41
41
  runs-on: ubuntu-latest
42
42
  steps:
43
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
44
- - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
43
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
44
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
45
45
  - name: Sync dependencies
46
- run: uv sync
46
+ run: uv sync --locked
47
47
  - name: Run mypy
48
48
  run: uv run mypy src/openapi_cli4ai/ --ignore-missing-imports
@@ -16,8 +16,8 @@ jobs:
16
16
  analyze:
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
20
- - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
19
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
20
+ - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
21
21
  with:
22
22
  languages: python
23
- - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
23
+ - uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
@@ -13,8 +13,8 @@ jobs:
13
13
  permissions:
14
14
  contents: read
15
15
  steps:
16
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
17
- - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
16
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
17
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
18
18
  - name: Build sdist and wheel
19
19
  run: uv build
20
20
  - name: Upload artifacts
@@ -42,7 +42,7 @@ jobs:
42
42
  name: dist
43
43
  path: dist/
44
44
  - name: Generate attestations
45
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
45
+ uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
46
46
  with:
47
47
  subject-path: dist/*
48
48
  - name: Publish to PyPI
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [0.5.1] - 2026-07-04
8
+
9
+ ### Fixed
10
+
11
+ - Ingest YAML OpenAPI specs served as `application/vnd.oai.openapi` (e.g. Codecov's schema endpoint), with a YAML fallback when a body advertised as JSON fails to decode (#22)
12
+ - `--version` now derives from installed package metadata instead of a hardcoded literal that had drifted from the real version (#22)
13
+
14
+ ### Changed
15
+
16
+ - Commit `uv.lock` and use `uv sync --locked` in CI for reproducible builds; Dependabot now updates the lockfile via the `uv` ecosystem (#20)
17
+ - Dependency updates: `typer` 0.26 (with test adjustments for its vendored `click`), plus the python-deps and GitHub Actions groups (#21, #19, #16)
18
+
7
19
  ## [0.5.0] - 2026-04-21
8
20
 
9
21
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openapi-cli4ai
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Turn any REST API with an OpenAPI spec into an AI-ready CLI
5
5
  Project-URL: Homepage, https://github.com/dbgorilla/openapi-cli4ai
6
6
  Project-URL: Repository, https://github.com/dbgorilla/openapi-cli4ai
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openapi-cli4ai"
7
- version = "0.5.0"
7
+ version = "0.5.1"
8
8
  description = "Turn any REST API with an OpenAPI spec into an AI-ready CLI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -44,7 +44,8 @@ openapi-cli4ai = "openapi_cli4ai.cli:app"
44
44
  dev = [
45
45
  "pytest>=8",
46
46
  "pytest-cov>=5",
47
- "mypy>=1.10,<2",
47
+ "click>=8,<9",
48
+ "mypy>=1.10,<3",
48
49
  "types-PyYAML>=6,<7",
49
50
  ]
50
51
 
@@ -26,6 +26,7 @@ import base64
26
26
  import binascii
27
27
  from email.utils import parsedate_to_datetime
28
28
  import hashlib
29
+ from importlib.metadata import version as _pkg_version, PackageNotFoundError
29
30
  import html as html_module # noqa: F401 (used by F3: OIDC callback HTML escaping)
30
31
  import json
31
32
  import os
@@ -56,8 +57,11 @@ from rich.panel import Panel # noqa: E402
56
57
  from rich.table import Table # noqa: E402
57
58
 
58
59
  # ── Constants ──────────────────────────────────────────────────────────────────
59
- VERSION = "0.4.0"
60
60
  APP_NAME = "openapi-cli4ai"
61
+ try:
62
+ VERSION = _pkg_version(APP_NAME)
63
+ except PackageNotFoundError: # running from a source checkout without an install
64
+ VERSION = "0.0.0+dev"
61
65
  CONFIG_FILE = Path.home() / ".openapi-cli4ai.toml"
62
66
  CACHE_DIR = Path.home() / ".cache" / APP_NAME
63
67
  CACHE_TTL = 3600 # 1 hour
@@ -458,11 +462,22 @@ def fetch_spec(profile: dict, refresh: bool = False) -> dict:
458
462
  f"Got HTML instead of OpenAPI spec from {spec_url} — the server may have a frontend catch-all route"
459
463
  )
460
464
 
461
- # Handle JSON or YAML
462
- if "yaml" in content_type or spec_url.endswith((".yaml", ".yml")):
465
+ # Handle JSON or YAML. Some servers serve a YAML spec under a
466
+ # non-obvious media type e.g. Codecov's schema endpoint returns
467
+ # "application/vnd.oai.openapi" (the standard OpenAPI YAML type),
468
+ # which contains neither "yaml" nor a ".yaml" suffix. Prefer JSON
469
+ # when the type says so (covers the "+json" variant), route known
470
+ # YAML types to the YAML parser, and fall back to YAML if a body
471
+ # not advertised as JSON fails to decode.
472
+ ct = content_type.lower()
473
+ is_yaml = "yaml" in ct or "vnd.oai.openapi" in ct or spec_url.endswith((".yaml", ".yml"))
474
+ if is_yaml and "json" not in ct:
463
475
  spec = yaml.safe_load(resp.text)
464
476
  else:
465
- spec = resp.json()
477
+ try:
478
+ spec = resp.json()
479
+ except json.JSONDecodeError:
480
+ spec = yaml.safe_load(resp.text)
466
481
 
467
482
  # Write cache atomically to prevent partial writes
468
483
  ensure_dirs()
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import base64
6
6
 
7
7
  import pytest
8
- from click.exceptions import Exit as ClickExit
8
+ from typer import Exit as ClickExit
9
9
 
10
10
 
11
11
  def test_auth_none(cli_module):
@@ -62,9 +62,9 @@ class TestOAuthBearer:
62
62
  }
63
63
  token_cache.write_text(json.dumps(token_data))
64
64
 
65
- import click
65
+ import typer
66
66
 
67
- with pytest.raises(click.exceptions.Exit):
67
+ with pytest.raises(typer.Exit):
68
68
  cli._oauth_bearer(profile, profile["auth"])
69
69
 
70
70
 
@@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
9
9
 
10
10
  import httpx
11
11
  import pytest
12
- from click.exceptions import Exit as ClickExit
12
+ from typer import Exit as ClickExit
13
13
 
14
14
  from openapi_cli4ai import cli as cli_mod
15
15
 
@@ -105,3 +105,81 @@ def test_fetch_spec_stale_cache_triggers_fetch(tmp_config, petstore_spec):
105
105
  mock_make_client.return_value = mock_ctx
106
106
  result = mod.fetch_spec(profile)
107
107
  assert result["info"]["title"] == petstore_spec["info"]["title"]
108
+
109
+
110
+ def _fetch_with_mocked_response(mod, profile, response):
111
+ """Run fetch_spec with _make_client mocked to return `response` from a fresh fetch."""
112
+ # raise_for_status() needs a request attached to the response.
113
+ response.request = httpx.Request("GET", profile.get("openapi_url", profile["base_url"]))
114
+ mock_client = MagicMock()
115
+ mock_client.get = MagicMock(return_value=response)
116
+ mock_ctx = MagicMock()
117
+ mock_ctx.__enter__ = MagicMock(return_value=mock_client)
118
+ mock_ctx.__exit__ = MagicMock(return_value=False)
119
+ with patch("openapi_cli4ai.cli._make_client", return_value=mock_ctx):
120
+ return mod.fetch_spec(profile)
121
+
122
+
123
+ def test_fetch_spec_parses_yaml_vnd_oai_media_type(tmp_config):
124
+ """Should parse a YAML spec served as application/vnd.oai.openapi (e.g. Codecov)."""
125
+ mod, _config_dir, _cache_dir = tmp_config
126
+ profile = {
127
+ "base_url": "https://api.codecov.io/api/v2",
128
+ "openapi_url": "https://api.codecov.io/api/v2/schema/",
129
+ "auth": {"type": "none"},
130
+ "verify_ssl": True,
131
+ "_name": "codecov",
132
+ }
133
+ yaml_spec = (
134
+ "openapi: 3.0.3\n"
135
+ "info:\n title: Codecov API\n version: 2.0.0\n"
136
+ "paths:\n /repos/:\n get:\n summary: List repos\n"
137
+ )
138
+ response = httpx.Response(
139
+ 200,
140
+ content=yaml_spec.encode(),
141
+ headers={"content-type": "application/vnd.oai.openapi; charset=utf-8"},
142
+ )
143
+ spec = _fetch_with_mocked_response(mod, profile, response)
144
+ assert spec["info"]["title"] == "Codecov API"
145
+ assert "/repos/" in spec["paths"]
146
+
147
+
148
+ def test_fetch_spec_falls_back_to_yaml_on_mislabeled_json(tmp_config):
149
+ """Should fall back to YAML when a body advertised as JSON is actually YAML."""
150
+ mod, _config_dir, _cache_dir = tmp_config
151
+ profile = {
152
+ "base_url": "https://example.com",
153
+ "openapi_url": "https://example.com/schema",
154
+ "auth": {"type": "none"},
155
+ "verify_ssl": True,
156
+ "_name": "mislabeled",
157
+ }
158
+ yaml_spec = "openapi: 3.0.3\ninfo:\n title: Mislabeled\n version: 1.0.0\npaths: {}\n"
159
+ response = httpx.Response(
160
+ 200,
161
+ content=yaml_spec.encode(),
162
+ headers={"content-type": "application/json"}, # server lies
163
+ )
164
+ spec = _fetch_with_mocked_response(mod, profile, response)
165
+ assert spec["info"]["title"] == "Mislabeled"
166
+
167
+
168
+ def test_fetch_spec_parses_vnd_oai_openapi_json_variant(tmp_config):
169
+ """The +json variant should still parse via the JSON path."""
170
+ mod, _config_dir, _cache_dir = tmp_config
171
+ profile = {
172
+ "base_url": "https://api.codecov.io/api/v2",
173
+ "openapi_url": "https://api.codecov.io/api/v2/schema/?format=json",
174
+ "auth": {"type": "none"},
175
+ "verify_ssl": True,
176
+ "_name": "codecovjson",
177
+ }
178
+ json_spec = json.dumps({"openapi": "3.0.3", "info": {"title": "Codecov API", "version": "2.0.0"}, "paths": {}})
179
+ response = httpx.Response(
180
+ 200,
181
+ content=json_spec.encode(),
182
+ headers={"content-type": "application/vnd.oai.openapi+json"},
183
+ )
184
+ spec = _fetch_with_mocked_response(mod, profile, response)
185
+ assert spec["info"]["title"] == "Codecov API"
@@ -1210,6 +1210,12 @@ class TestMainCallback:
1210
1210
  assert "openapi-cli4ai" in result.output
1211
1211
  assert mod.VERSION in result.output
1212
1212
 
1213
+ def test_version_derived_from_package_metadata(self, cli_module):
1214
+ """VERSION comes from installed package metadata, not a hardcoded literal."""
1215
+ from importlib.metadata import version
1216
+
1217
+ assert cli_module.VERSION == version("openapi-cli4ai")
1218
+
1213
1219
  def test_no_subcommand_shows_help(self, tmp_config):
1214
1220
  mod, tmp_path, cache_dir = tmp_config
1215
1221
 
@@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch
10
10
  import httpx
11
11
  import pytest
12
12
  import tomli_w
13
- from click.exceptions import Exit as ClickExit
13
+ from typer import Exit as ClickExit
14
14
  from typer.testing import CliRunner
15
15
 
16
16
  from openapi_cli4ai.cli import app
@@ -9,7 +9,7 @@ import time
9
9
  from unittest.mock import MagicMock, patch
10
10
 
11
11
  import pytest
12
- from click.exceptions import Exit as ClickExit
12
+ from typer import Exit as ClickExit
13
13
 
14
14
 
15
15
  # ── Device flow endpoint discovery ────────────────────────────────────────────
@@ -10,7 +10,7 @@ import inspect
10
10
  import socket
11
11
  from unittest.mock import MagicMock, patch
12
12
 
13
- import click
13
+ import typer
14
14
  import pytest
15
15
 
16
16
  from openapi_cli4ai import cli as cli_mod
@@ -41,7 +41,7 @@ class TestSpecificExceptions:
41
41
  mock_client.get.side_effect = httpx.ConnectError("mocked connection error")
42
42
  mock_mc.return_value = mock_client
43
43
 
44
- with pytest.raises(click.exceptions.Exit):
44
+ with pytest.raises(typer.Exit):
45
45
  cli.fetch_spec(profile, refresh=True)
46
46
 
47
47
  def test_try_refresh_token_catches_specific_errors(self, tmp_config):
@@ -113,7 +113,7 @@ class TestOIDCServerCleanup:
113
113
  sock.listen(1)
114
114
 
115
115
  # Calling _oidc_login_browser on this port should exit cleanly
116
- with pytest.raises(click.exceptions.Exit):
116
+ with pytest.raises(typer.Exit):
117
117
  cli_mod._oidc_login_browser("http://auth.example.com/authorize", port, "test_state")
118
118
  finally:
119
119
  sock.close()
@@ -8,7 +8,7 @@ import json
8
8
  import time
9
9
 
10
10
  import pytest
11
- from click.exceptions import Exit as ClickExit
11
+ from typer import Exit as ClickExit
12
12
 
13
13
 
14
14
  def test_oidc_auth_returns_cached_token(cli_module, tmp_config):
@@ -11,7 +11,7 @@ import stat
11
11
  import urllib.parse
12
12
  from unittest.mock import patch
13
13
 
14
- import click
14
+ import typer
15
15
  import pytest
16
16
  from typer.testing import CliRunner
17
17
 
@@ -317,7 +317,7 @@ class TestOIDCStateValidation:
317
317
  # Mock typer.prompt to return a URL with wrong state
318
318
  wrong_url = "http://localhost:8484/callback?code=abc123&state=WRONG"
319
319
  with patch("typer.prompt", return_value=wrong_url):
320
- with pytest.raises(click.exceptions.Exit):
320
+ with pytest.raises(typer.Exit):
321
321
  cli_mod._oidc_login_no_browser("http://auth.example.com", expected_state="CORRECT")
322
322
 
323
323
  def test_no_browser_accepts_correct_state(self):
@@ -331,7 +331,7 @@ class TestOIDCStateValidation:
331
331
  """_oidc_login_no_browser should reject a redirect URL with no state."""
332
332
  no_state_url = "http://localhost:8484/callback?code=abc123"
333
333
  with patch("typer.prompt", return_value=no_state_url):
334
- with pytest.raises(click.exceptions.Exit):
334
+ with pytest.raises(typer.Exit):
335
335
  cli_mod._oidc_login_no_browser("http://auth.example.com", expected_state="EXPECTED")
336
336
 
337
337
 
@@ -489,7 +489,7 @@ class TestProfileFallbackWarning:
489
489
  }
490
490
  )
491
491
  monkeypatch.setenv("OAC_PROFILE", "nonexistent")
492
- with pytest.raises(click.exceptions.Exit):
492
+ with pytest.raises(typer.Exit):
493
493
  cli.get_active_profile()
494
494
  captured = capsys.readouterr()
495
495
  assert "not found" in captured.err.lower()
@@ -635,21 +635,21 @@ class TestConfigCorruption:
635
635
  """A corrupt TOML config should exit with error, not silently return empty."""
636
636
  cli, config_path, cache_dir = tmp_config
637
637
  cli.CONFIG_FILE.write_text("this is not valid toml {{{")
638
- with pytest.raises(click.exceptions.Exit):
638
+ with pytest.raises(typer.Exit):
639
639
  cli.load_profiles()
640
640
 
641
641
  def test_wrong_shape_profiles_exits(self, tmp_config):
642
642
  """Config with profiles = 'string' should exit, not crash."""
643
643
  cli, config_path, cache_dir = tmp_config
644
644
  cli.CONFIG_FILE.write_text('profiles = "oops"\n')
645
- with pytest.raises(click.exceptions.Exit):
645
+ with pytest.raises(typer.Exit):
646
646
  cli.load_profiles()
647
647
 
648
648
  def test_wrong_shape_nested_profile_exits(self, tmp_config):
649
649
  """Config with [profiles] bad = 'string' should exit, not crash."""
650
650
  cli, config_path, cache_dir = tmp_config
651
651
  cli.CONFIG_FILE.write_text('[profiles]\nbad = "oops"\n')
652
- with pytest.raises(click.exceptions.Exit):
652
+ with pytest.raises(typer.Exit):
653
653
  cli.get_active_profile()
654
654
 
655
655
 
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
  import inspect
12
12
  from unittest.mock import MagicMock, patch
13
13
 
14
- import click.exceptions
14
+ import typer
15
15
  import httpx
16
16
  import pytest
17
17
 
@@ -65,7 +65,7 @@ class TestTokenExchangeHardening:
65
65
  mock_ctx.__exit__ = MagicMock(return_value=False)
66
66
 
67
67
  with patch.object(cli_module, "_make_client", return_value=mock_ctx):
68
- with pytest.raises(click.exceptions.Exit):
68
+ with pytest.raises(typer.Exit):
69
69
  cli_module._token_exchange(
70
70
  {"access_token": "tok"},
71
71
  auth_config,
@@ -128,7 +128,7 @@ class TestDeviceLoginHardening:
128
128
  patch.object(cli_module, "_device_discover_endpoints", return_value=endpoints),
129
129
  patch.object(cli_module, "_make_client", return_value=mock_ctx),
130
130
  ):
131
- with pytest.raises(click.exceptions.Exit):
131
+ with pytest.raises(typer.Exit):
132
132
  cli_module._device_login(auth_config, "test-profile", profile)
133
133
 
134
134