openapi-cli4ai 0.6.0__tar.gz → 0.7.0__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.
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/workflows/ci.yml +3 -3
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/workflows/codeql.yml +2 -2
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/workflows/publish.yml +3 -3
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/workflows/validate-profiles.yml +3 -1
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/CHANGELOG.md +22 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/CONTRIBUTING.md +17 -6
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/PKG-INFO +4 -2
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/README.md +2 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/profiles/README.md +41 -2
- openapi_cli4ai-0.7.0/profiles/index.json +33 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/pyproject.toml +16 -5
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/src/openapi_cli4ai/__init__.py +1 -1
- openapi_cli4ai-0.7.0/src/openapi_cli4ai/_state.py +22 -0
- openapi_cli4ai-0.7.0/src/openapi_cli4ai/_ui.py +16 -0
- openapi_cli4ai-0.7.0/src/openapi_cli4ai/catalog.py +247 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/src/openapi_cli4ai/cli.py +223 -513
- openapi_cli4ai-0.7.0/src/openapi_cli4ai/config.py +254 -0
- openapi_cli4ai-0.7.0/src/openapi_cli4ai/validator.py +253 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/conftest.py +13 -3
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_auth_flow_security.py +2 -2
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_auth_flows.py +5 -8
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_caching.py +1 -1
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_catalog.py +89 -15
- openapi_cli4ai-0.7.0/tests/test_catalog_remote.py +162 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_device_flow.py +0 -1
- openapi_cli4ai-0.7.0/tests/test_domain_verified.py +122 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_error_handling.py +10 -8
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_features.py +52 -51
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_foundation.py +12 -10
- openapi_cli4ai-0.7.0/tests/test_profile_anywhere.py +79 -0
- openapi_cli4ai-0.7.0/tests/test_profiles_d.py +119 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_schema_composition.py +0 -1
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_security.py +5 -7
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_v040_hardening.py +11 -10
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/uv.lock +94 -78
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/CODEOWNERS +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/dependabot.yml +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/pull_request_template.md +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.github/release.yml +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.gitignore +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/.pre-commit-config.yaml +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/LICENSE +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/SECURITY.md +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/demo.gif +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/examples/profiles.toml.example +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/openapi-cli4ai +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/profiles/community/.gitkeep +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/profiles/community/xquik.toml +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/profiles/verified/petstore.toml +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/src/openapi_cli4ai/__main__.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/src/openapi_cli4ai/py.typed +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/__init__.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/fixtures/petstore_spec.json +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_auth.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_cli_commands.py +1 -1
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_commands.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_coverage_gaps.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_integration.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_oidc.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_profile_management.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_run_command.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_spec_parsing.py +0 -0
- {openapi_cli4ai-0.6.0 → openapi_cli4ai-0.7.0}/tests/test_sse_streaming.py +0 -0
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
python-version: ["3.11", "3.12", "3.13"]
|
|
18
18
|
steps:
|
|
19
19
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
|
-
- uses: astral-sh/setup-uv@
|
|
20
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
21
21
|
with:
|
|
22
22
|
enable-cache: true
|
|
23
23
|
- name: Set up Python ${{ matrix.python-version }}
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
steps:
|
|
33
33
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
34
|
-
- uses: astral-sh/setup-uv@
|
|
34
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
35
35
|
- name: Sync dependencies
|
|
36
36
|
run: uv sync --locked
|
|
37
37
|
- name: Ruff check
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
runs-on: ubuntu-latest
|
|
44
44
|
steps:
|
|
45
45
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
46
|
-
- uses: astral-sh/setup-uv@
|
|
46
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
47
47
|
- name: Sync dependencies
|
|
48
48
|
run: uv sync --locked
|
|
49
49
|
- name: Run mypy
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
18
|
steps:
|
|
19
19
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
20
|
-
- uses: github/codeql-action/init@
|
|
20
|
+
- uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
|
21
21
|
with:
|
|
22
22
|
languages: python
|
|
23
|
-
- uses: github/codeql-action/analyze@
|
|
23
|
+
- uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
contents: read
|
|
15
15
|
steps:
|
|
16
16
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
17
|
-
- uses: astral-sh/setup-uv@
|
|
17
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
18
18
|
- name: Build sdist and wheel
|
|
19
19
|
run: uv build
|
|
20
20
|
- name: Upload artifacts
|
|
@@ -42,8 +42,8 @@ jobs:
|
|
|
42
42
|
name: dist
|
|
43
43
|
path: dist/
|
|
44
44
|
- name: Generate attestations
|
|
45
|
-
uses: actions/attest-build-provenance@
|
|
45
|
+
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
|
46
46
|
with:
|
|
47
47
|
subject-path: dist/*
|
|
48
48
|
- name: Publish to PyPI
|
|
49
|
-
uses: pypa/gh-action-pypi-publish@
|
|
49
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -19,6 +19,8 @@ jobs:
|
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
steps:
|
|
21
21
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
22
|
-
- uses: astral-sh/setup-uv@
|
|
22
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
23
23
|
- name: Validate catalog profiles
|
|
24
24
|
run: uv run --locked openapi-cli4ai catalog validate --all
|
|
25
|
+
- name: Check profiles/index.json is current
|
|
26
|
+
run: uv run --locked openapi-cli4ai catalog index --check
|
|
@@ -4,6 +4,28 @@ 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.7.0] - 2026-09-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `--profile` is accepted after the subcommand (`openapi-cli4ai endpoints --profile x`); long form only, since `-p` is reused by subcommands (#52)
|
|
12
|
+
- `catalog install --dry-run` prints the exact profile file that would be written and changes nothing (#53)
|
|
13
|
+
- `catalog uninstall <name>` removes an installed catalog profile plus its cached spec and token, with confirm prompt and `--force` (#54)
|
|
14
|
+
- Drop-in profiles: one file per profile under `~/.openapi-cli4ai/profiles.d/<name>.toml`, read alongside `~/.openapi-cli4ai.toml` (a drop-in overrides a same-named entry). `catalog install` writes there. No migration needed (#56)
|
|
15
|
+
- Remote catalog index: `catalog list`/`search`/`show`/`install` merge `profiles/index.json` from `main` over the bundled catalog (3 s timeout, cached one hour, silent fallback to cache then bundled; every remote entry is re-validated). `OAC_CATALOG_OFFLINE=1` disables it. New `catalog index` command generates the committed index; CI checks it is current (#57)
|
|
16
|
+
- DNS TXT domain verification: a profile may set `domain_verified = true`; `catalog validate` resolves `_openapi-cli4ai.<domain>` and requires `github=<maintainer>`. Shown as a `✓ dns` badge, separate from the tier (#58)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `cli.py` split into `config.py`, `catalog.py`, `validator.py` plus small state/UI modules; behavior unchanged (#55)
|
|
21
|
+
- Adopted ruff 0.16's default rule set; the rule selection is no longer implicit (#50, #51)
|
|
22
|
+
- Dependency updates: `typer` 0.27.2, `click` 8.5, `python-dotenv`, `mypy`, `types-PyYAML`, and the GitHub Actions group (#49, #50)
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Rich swallowed TOML table headers such as `[profiles.x]` in `profile show` output; now rendered as plain text (#53)
|
|
27
|
+
- File paths in install output are never wrapped across lines (#56)
|
|
28
|
+
|
|
7
29
|
## [0.6.0] - 2026-07-31
|
|
8
30
|
|
|
9
31
|
### Added
|
|
@@ -16,12 +16,17 @@ Thanks for your interest in contributing!
|
|
|
16
16
|
src/openapi_cli4ai/
|
|
17
17
|
__init__.py # Package exports
|
|
18
18
|
__main__.py # python -m support
|
|
19
|
-
cli.py #
|
|
19
|
+
cli.py # Typer commands, HTTP/auth flows, spec fetching
|
|
20
|
+
config.py # Config file, cache dir, active-profile precedence
|
|
21
|
+
catalog.py # Bundled profile catalog: load, find, map to a profile
|
|
22
|
+
validator.py # Catalog profile validator (shared by `catalog validate` and CI)
|
|
23
|
+
_state.py # Runtime flags set by the root command (--verbose, --profile, ...)
|
|
24
|
+
_ui.py # Shared Rich consoles and the verbose logger
|
|
20
25
|
openapi-cli4ai # Standalone shim (imports from package)
|
|
21
26
|
tests/ # pytest tests
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
Commands live in `src/openapi_cli4ai/cli.py`; it imports the helpers it needs from the service modules, so tests can patch either the service module or the `cli` binding. The standalone `openapi-cli4ai` script is a thin shim that imports from the package.
|
|
25
30
|
|
|
26
31
|
## Testing
|
|
27
32
|
|
|
@@ -48,10 +53,16 @@ pytest tests/ -m "not integration" -v
|
|
|
48
53
|
3. Keep the `description` factual. `source` must be the API's own developer or
|
|
49
54
|
docs URL (same domain as `base_url`).
|
|
50
55
|
4. Validate locally: `uv run openapi-cli4ai catalog validate profiles/community/<slug>.toml`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
5. Regenerate the index and commit it: `uv run openapi-cli4ai catalog index`
|
|
57
|
+
6. Optional: claim the domain-verified badge by setting `domain_verified = true`
|
|
58
|
+
and publishing a `_openapi-cli4ai.<domain>` TXT record naming you; see
|
|
59
|
+
"Domain verification" in [`profiles/README.md`](profiles/README.md).
|
|
60
|
+
|
|
61
|
+
CI runs `catalog validate --all` and `catalog index --check` on every PR touching
|
|
62
|
+
`profiles/`, checking the fields, a live spec fetch, ownership, absence of inline
|
|
63
|
+
secrets, any `domain_verified` claim against DNS, and that `profiles/index.json`
|
|
64
|
+
is current — errors appear inline on the PR. Once merged, the profile is visible to every installed CLI within an hour
|
|
65
|
+
via the remote index (see `profiles/README.md`, "Freshness"). Profiles that are primarily promotional will be closed
|
|
55
66
|
without review.
|
|
56
67
|
|
|
57
68
|
## Reporting Issues
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: openapi-cli4ai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -237,6 +237,8 @@ openapi-cli4ai logout
|
|
|
237
237
|
|
|
238
238
|
Profiles are stored in `~/.openapi-cli4ai.toml`. Secrets are referenced via environment variables — never stored in the config file.
|
|
239
239
|
|
|
240
|
+
Profiles can also live one-per-file under `~/.openapi-cli4ai/profiles.d/<name>.toml`, where the file holds the profile's keys directly (no `[profiles.<name>]` header) and the file name is the profile name. `catalog install` writes there so installing, upgrading and uninstalling a profile touch only that file. Both locations are read; a drop-in overrides a same-named entry in `~/.openapi-cli4ai.toml`. `active_profile` is only read from `~/.openapi-cli4ai.toml`.
|
|
241
|
+
|
|
240
242
|
```toml
|
|
241
243
|
active_profile = "myapi"
|
|
242
244
|
|
|
@@ -208,6 +208,8 @@ openapi-cli4ai logout
|
|
|
208
208
|
|
|
209
209
|
Profiles are stored in `~/.openapi-cli4ai.toml`. Secrets are referenced via environment variables — never stored in the config file.
|
|
210
210
|
|
|
211
|
+
Profiles can also live one-per-file under `~/.openapi-cli4ai/profiles.d/<name>.toml`, where the file holds the profile's keys directly (no `[profiles.<name>]` header) and the file name is the profile name. `catalog install` writes there so installing, upgrading and uninstalling a profile touch only that file. Both locations are read; a drop-in overrides a same-named entry in `~/.openapi-cli4ai.toml`. `active_profile` is only read from `~/.openapi-cli4ai.toml`.
|
|
212
|
+
|
|
211
213
|
```toml
|
|
212
214
|
active_profile = "myapi"
|
|
213
215
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Ready-made **profiles** for public APIs. A profile is the small bit of config
|
|
4
4
|
`openapi-cli4ai` needs to talk to an API: a base URL, where its OpenAPI spec
|
|
5
5
|
lives, and how it authenticates. The catalog is bundled into the package, so
|
|
6
|
-
these commands work offline
|
|
6
|
+
these commands work offline; when online they also pick up profiles merged to
|
|
7
|
+
`main` since your release (see [Freshness](#freshness)):
|
|
7
8
|
|
|
8
9
|
```bash
|
|
9
10
|
openapi-cli4ai catalog search cov # find profiles
|
|
@@ -26,12 +27,30 @@ a **tier**, shown next to every entry, not a gate on being listed.
|
|
|
26
27
|
| **Verified** | `verified/` | A maintainer confirmed the spec loads and the auth flow works. |
|
|
27
28
|
| **Community** | `community/` | Contributed via PR and passed automated validation. Not manually vetted. |
|
|
28
29
|
|
|
30
|
+
Independently of the tier, a profile can carry a **domain-verified** badge
|
|
31
|
+
(`✓ dns` in listings): the owner of the API's domain has published a DNS TXT
|
|
32
|
+
record naming the maintainer, so the listing is backed by domain control, not
|
|
33
|
+
just a self-declared `source`. See [Domain verification](#domain-verification).
|
|
34
|
+
|
|
29
35
|
New submissions go to `community/`; a maintainer may promote a profile to
|
|
30
36
|
`verified/` after checking it end to end. Installing a **community** profile
|
|
31
37
|
prompts for confirmation (it shows the `base_url` your credentials would be
|
|
32
38
|
sent to); **verified** profiles install without a prompt. Use `--yes` to skip
|
|
33
39
|
the prompt in scripts.
|
|
34
40
|
|
|
41
|
+
## Freshness
|
|
42
|
+
|
|
43
|
+
The bundled catalog is the offline baseline. On top of it, `catalog list`,
|
|
44
|
+
`search`, `show` and `install` fetch [`index.json`](index.json) from this
|
|
45
|
+
repository's `main` branch (3 s timeout, cached for an hour under
|
|
46
|
+
`~/.cache/openapi-cli4ai/`) and merge it in, so a profile is usable as soon as
|
|
47
|
+
its PR merges. Any failure falls back to the cached index, then to the bundled
|
|
48
|
+
copy, silently. Every remote entry is re-checked with the offline validator
|
|
49
|
+
before use; a bad one is skipped. Set `OAC_CATALOG_OFFLINE=1` to never fetch.
|
|
50
|
+
|
|
51
|
+
`index.json` is generated from the TOML files by `openapi-cli4ai catalog index`
|
|
52
|
+
and committed; CI fails a PR whose index is stale.
|
|
53
|
+
|
|
35
54
|
## Profile format
|
|
36
55
|
|
|
37
56
|
One profile per file, named `<slug>.toml`:
|
|
@@ -45,6 +64,8 @@ source = "https://example.com/docs" # the API's official docs (same domain
|
|
|
45
64
|
base_url = "https://api.example.com"
|
|
46
65
|
openapi_url = "https://api.example.com/openapi.json" # or: openapi_path = "/openapi.json"
|
|
47
66
|
|
|
67
|
+
domain_verified = true # optional: see "Domain verification" below
|
|
68
|
+
|
|
48
69
|
[auth]
|
|
49
70
|
type = "api-key" # none | bearer | oidc | device | api-key | basic
|
|
50
71
|
env_var = "EXAMPLE_API_KEY" # reference secrets by env var — never inline them
|
|
@@ -55,6 +76,22 @@ Auth field names match the CLI's runtime config; see
|
|
|
55
76
|
[`../examples/profiles.toml.example`](../examples/profiles.toml.example) for
|
|
56
77
|
each auth type.
|
|
57
78
|
|
|
79
|
+
## Domain verification
|
|
80
|
+
|
|
81
|
+
To claim the `✓ dns` badge, set `domain_verified = true` in the profile and have
|
|
82
|
+
the API's domain owner publish this TXT record (registrable domain of
|
|
83
|
+
`base_url`, e.g. `api.acme.co.uk` → `acme.co.uk`):
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
_openapi-cli4ai.acme.co.uk. IN TXT "github=<maintainer>"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`<maintainer>` must equal the profile's `maintainer` field (case-insensitive).
|
|
90
|
+
`catalog validate` resolves the record and fails the PR if it is missing or
|
|
91
|
+
names someone else; the claim is re-checked on every profile change. Remove the
|
|
92
|
+
claim (or the record) to drop the badge. The lookup needs `dnspython`, which
|
|
93
|
+
`uv sync` installs for contributors; end users never resolve DNS.
|
|
94
|
+
|
|
58
95
|
## Contributing a profile
|
|
59
96
|
|
|
60
97
|
1. Create `community/<slug>.toml` in the format above.
|
|
@@ -62,10 +99,12 @@ each auth type.
|
|
|
62
99
|
3. Keep `description` factual. `source` must be the API's own developer/docs
|
|
63
100
|
URL (same registrable domain as `base_url`).
|
|
64
101
|
|
|
65
|
-
Validate before opening the PR — the CLI is the single source of truth
|
|
102
|
+
Validate before opening the PR — the CLI is the single source of truth — and
|
|
103
|
+
regenerate the index:
|
|
66
104
|
|
|
67
105
|
```bash
|
|
68
106
|
uv run openapi-cli4ai catalog validate profiles/community/<slug>.toml
|
|
107
|
+
uv run openapi-cli4ai catalog index
|
|
69
108
|
```
|
|
70
109
|
|
|
71
110
|
CI runs `catalog validate --all` on every PR touching `profiles/`. It checks the
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"profiles": [
|
|
3
|
+
{
|
|
4
|
+
"auth": {
|
|
5
|
+
"env_var": "XQUIK_API_KEY",
|
|
6
|
+
"header": "x-api-key",
|
|
7
|
+
"type": "api-key"
|
|
8
|
+
},
|
|
9
|
+
"base_url": "https://xquik.com",
|
|
10
|
+
"description": "Xquik REST API",
|
|
11
|
+
"maintainer": "kriptoburak",
|
|
12
|
+
"name": "xquik",
|
|
13
|
+
"openapi_url": "https://xquik.com/openapi.json",
|
|
14
|
+
"slug": "xquik",
|
|
15
|
+
"source": "https://xquik.com",
|
|
16
|
+
"tier": "community"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"auth": {
|
|
20
|
+
"type": "none"
|
|
21
|
+
},
|
|
22
|
+
"base_url": "https://petstore3.swagger.io/api/v3",
|
|
23
|
+
"description": "Swagger Petstore \u2014 the OpenAPI 3 sample API",
|
|
24
|
+
"maintainer": "dbgorilla",
|
|
25
|
+
"name": "petstore",
|
|
26
|
+
"openapi_url": "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
27
|
+
"slug": "petstore",
|
|
28
|
+
"source": "https://petstore3.swagger.io",
|
|
29
|
+
"tier": "verified"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"version": 1
|
|
33
|
+
}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openapi-cli4ai"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
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"
|
|
@@ -46,12 +46,14 @@ dev = [
|
|
|
46
46
|
"pytest>=8",
|
|
47
47
|
"pytest-cov>=5",
|
|
48
48
|
"click>=8,<9",
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
"ruff>=0.15,<0.16",
|
|
49
|
+
# CI runs the locked ruff (uv run) instead of uvx-latest so lint is
|
|
50
|
+
# deterministic; a bump comes via Dependabot and is gated by CI.
|
|
51
|
+
"ruff>=0.15,<0.17",
|
|
53
52
|
"mypy>=1.10,<3",
|
|
54
53
|
"types-PyYAML>=6,<7",
|
|
54
|
+
# DNS TXT domain verification in `catalog validate` (CI + contributors);
|
|
55
|
+
# end users never resolve DNS, so this is not a runtime dependency.
|
|
56
|
+
"dnspython>=2.4,<3",
|
|
55
57
|
]
|
|
56
58
|
|
|
57
59
|
[tool.hatch.build.targets.wheel]
|
|
@@ -66,6 +68,15 @@ packages = ["src/openapi_cli4ai"]
|
|
|
66
68
|
line-length = 120
|
|
67
69
|
target-version = "py311"
|
|
68
70
|
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
# Rule set = ruff's defaults for the locked version. Drift is guarded by the
|
|
73
|
+
# version cap on the dev dependency plus uv.lock; a ruff bump arrives via
|
|
74
|
+
# Dependabot and is gated by CI, so new rules are adopted deliberately.
|
|
75
|
+
# RUF059 (unused unpacked variable) is ignored in tests: fixtures return
|
|
76
|
+
# tuples like `mod, tmp_path, cache_dir = tmp_config` and unpacking the
|
|
77
|
+
# full shape is clearer than `_`-prefixing whichever half a test skips.
|
|
78
|
+
per-file-ignores = { "tests/**" = ["RUF059"] }
|
|
79
|
+
|
|
69
80
|
[tool.pytest.ini_options]
|
|
70
81
|
testpaths = ["tests"]
|
|
71
82
|
markers = ["integration: live API tests (may be slow)"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Runtime settings set once by the root command and read by every layer.
|
|
2
|
+
|
|
3
|
+
Module globals on purpose: the CLI is a single process handling one command,
|
|
4
|
+
and tests set these directly (``_state._verbose_mode = True``).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
_insecure_mode = False
|
|
10
|
+
_verbose_mode = False
|
|
11
|
+
_timeout_seconds = 60.0
|
|
12
|
+
_max_retries = 0
|
|
13
|
+
_profile_override: str | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_insecure_mode(insecure: bool) -> None:
|
|
17
|
+
global _insecure_mode
|
|
18
|
+
_insecure_mode = insecure
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_verify_ssl() -> bool:
|
|
22
|
+
return not _insecure_mode
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Shared Rich consoles and the verbose logger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from openapi_cli4ai import _state
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
err_console = Console(stderr=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _verbose(msg: str) -> None:
|
|
14
|
+
"""Print a verbose message to stderr if verbose mode is enabled."""
|
|
15
|
+
if _state._verbose_mode:
|
|
16
|
+
err_console.print(f"[dim]> {msg}[/dim]")
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Profile catalog: ready-made API profiles (load, search, map to a runtime profile).
|
|
2
|
+
|
|
3
|
+
Two sources, merged: the catalog bundled in the wheel (always available, so
|
|
4
|
+
every command works offline) and a remote index published from the main
|
|
5
|
+
branch, so a newly merged profile is visible before the next release. The
|
|
6
|
+
remote index is cached for CATALOG_INDEX_TTL seconds and any failure falls
|
|
7
|
+
back to the cache, then to the bundled copy.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.resources
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
17
|
+
import tomllib
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
|
|
24
|
+
from openapi_cli4ai import config
|
|
25
|
+
from openapi_cli4ai._ui import _verbose as _log
|
|
26
|
+
from openapi_cli4ai._ui import console
|
|
27
|
+
|
|
28
|
+
CATALOG_TIERS = ("verified", "community")
|
|
29
|
+
_CATALOG_META_FIELDS = ("name", "description", "maintainer", "source", "domain_verified")
|
|
30
|
+
|
|
31
|
+
# Remote index: generated by `catalog index` and committed to profiles/index.json.
|
|
32
|
+
CATALOG_INDEX_URL = "https://raw.githubusercontent.com/dbgorilla/openapi-cli4ai/main/profiles/index.json"
|
|
33
|
+
CATALOG_INDEX_TTL = 3600 # seconds
|
|
34
|
+
CATALOG_INDEX_VERSION = 1
|
|
35
|
+
_CATALOG_INDEX_MAX_BYTES = 1024 * 1024
|
|
36
|
+
_CATALOG_INDEX_TIMEOUT = httpx.Timeout(3.0, connect=2.0) # never make an offline user wait
|
|
37
|
+
# Set to any value to skip the remote index entirely (CI, air-gapped, tests).
|
|
38
|
+
CATALOG_OFFLINE_ENV = "OAC_CATALOG_OFFLINE"
|
|
39
|
+
_SLUG_RE = re.compile(r"[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _catalog_root() -> Any:
|
|
43
|
+
"""Locate the catalog: bundled in the wheel, else profiles/ in a source checkout."""
|
|
44
|
+
try:
|
|
45
|
+
bundled = importlib.resources.files("openapi_cli4ai") / "_catalog"
|
|
46
|
+
if bundled.is_dir():
|
|
47
|
+
return bundled
|
|
48
|
+
except (ModuleNotFoundError, FileNotFoundError, AttributeError, TypeError):
|
|
49
|
+
pass
|
|
50
|
+
dev = Path(__file__).resolve().parents[2] / "profiles"
|
|
51
|
+
return dev if dev.is_dir() else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_catalog(remote: bool = True) -> list[dict]:
|
|
55
|
+
"""Return every catalog profile as a dict with _tier and _slug attached.
|
|
56
|
+
|
|
57
|
+
Bundled entries first, then the remote index merged on top (a remote entry
|
|
58
|
+
replaces a bundled one with the same slug). `remote=False` returns only the
|
|
59
|
+
bundled copy; `catalog validate --all` and `catalog index` use that so they
|
|
60
|
+
describe the files in this checkout, not whatever main currently publishes.
|
|
61
|
+
"""
|
|
62
|
+
entries = _load_bundled_catalog()
|
|
63
|
+
if not remote:
|
|
64
|
+
return entries
|
|
65
|
+
remote_entries = _fetch_remote_index()
|
|
66
|
+
if not remote_entries:
|
|
67
|
+
return entries
|
|
68
|
+
merged = {e["_slug"]: e for e in entries}
|
|
69
|
+
for entry in remote_entries:
|
|
70
|
+
merged[entry["_slug"]] = entry
|
|
71
|
+
return list(merged.values())
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _load_bundled_catalog() -> list[dict]:
|
|
75
|
+
"""Read the catalog shipped in the wheel (or profiles/ in a source checkout)."""
|
|
76
|
+
root = _catalog_root()
|
|
77
|
+
entries: list[dict] = []
|
|
78
|
+
if root is None:
|
|
79
|
+
return entries
|
|
80
|
+
for tier in CATALOG_TIERS:
|
|
81
|
+
tier_dir = root / tier
|
|
82
|
+
if not tier_dir.is_dir():
|
|
83
|
+
continue
|
|
84
|
+
for item in sorted(tier_dir.iterdir(), key=lambda p: p.name):
|
|
85
|
+
if not item.name.endswith(".toml"):
|
|
86
|
+
continue
|
|
87
|
+
try:
|
|
88
|
+
entry = tomllib.loads(item.read_text(encoding="utf-8"))
|
|
89
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
90
|
+
continue
|
|
91
|
+
entry["_tier"] = tier
|
|
92
|
+
entry["_slug"] = item.name[: -len(".toml")]
|
|
93
|
+
entries.append(entry)
|
|
94
|
+
return entries
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Remote index ──────────────────────────────────────────────────────────────
|
|
98
|
+
def build_index(entries: list[dict]) -> dict:
|
|
99
|
+
"""Serialisable, deterministic index of catalog entries (no timestamps, sorted)."""
|
|
100
|
+
profiles = []
|
|
101
|
+
for entry in sorted(entries, key=lambda e: (e.get("_tier", ""), e.get("_slug", ""))):
|
|
102
|
+
record = {k: v for k, v in entry.items() if not k.startswith("_")}
|
|
103
|
+
record["tier"] = entry.get("_tier")
|
|
104
|
+
record["slug"] = entry.get("_slug")
|
|
105
|
+
profiles.append(record)
|
|
106
|
+
return {"version": CATALOG_INDEX_VERSION, "profiles": profiles}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def index_json(entries: list[dict]) -> str:
|
|
110
|
+
return json.dumps(build_index(entries), indent=2, sort_keys=True) + "\n"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _index_to_entries(index: Any) -> list[dict] | None:
|
|
114
|
+
"""Turn a parsed index into catalog entries. None if the document is malformed.
|
|
115
|
+
|
|
116
|
+
Every entry must pass the offline validator; a bad one is skipped, not fatal,
|
|
117
|
+
so one broken profile on main cannot blank the catalog for everyone.
|
|
118
|
+
"""
|
|
119
|
+
# Local import: validator imports this module for _catalog_to_profile.
|
|
120
|
+
from openapi_cli4ai.validator import _validate_catalog_entry
|
|
121
|
+
|
|
122
|
+
if not isinstance(index, dict) or index.get("version") != CATALOG_INDEX_VERSION:
|
|
123
|
+
return None
|
|
124
|
+
profiles = index.get("profiles")
|
|
125
|
+
if not isinstance(profiles, list):
|
|
126
|
+
return None
|
|
127
|
+
entries: list[dict] = []
|
|
128
|
+
for record in profiles:
|
|
129
|
+
if not isinstance(record, dict):
|
|
130
|
+
continue
|
|
131
|
+
entry = dict(record)
|
|
132
|
+
tier = entry.pop("tier", None)
|
|
133
|
+
slug = entry.pop("slug", None)
|
|
134
|
+
if tier not in CATALOG_TIERS or not isinstance(slug, str) or not _SLUG_RE.fullmatch(slug):
|
|
135
|
+
continue
|
|
136
|
+
entry["_tier"] = tier
|
|
137
|
+
entry["_slug"] = slug
|
|
138
|
+
errors, _warnings = _validate_catalog_entry(entry, check_spec=False)
|
|
139
|
+
if errors:
|
|
140
|
+
_log(f"catalog index: skipping '{slug}': {errors[0]}")
|
|
141
|
+
continue
|
|
142
|
+
entries.append(entry)
|
|
143
|
+
return entries
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _index_cache_path() -> Path:
|
|
147
|
+
return config.CACHE_DIR / "catalog_index.json"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _download_index() -> Any:
|
|
151
|
+
"""GET the remote index. Raises httpx.HTTPError or ValueError on any problem."""
|
|
152
|
+
with httpx.Client(timeout=_CATALOG_INDEX_TIMEOUT, follow_redirects=False) as client:
|
|
153
|
+
resp = client.get(CATALOG_INDEX_URL)
|
|
154
|
+
resp.raise_for_status()
|
|
155
|
+
if len(resp.content) > _CATALOG_INDEX_MAX_BYTES:
|
|
156
|
+
raise ValueError(f"index exceeds {_CATALOG_INDEX_MAX_BYTES // 1024}KB")
|
|
157
|
+
return resp.json()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _read_index_cache() -> tuple[float, Any] | None:
|
|
161
|
+
try:
|
|
162
|
+
cached = json.loads(_index_cache_path().read_text(encoding="utf-8"))
|
|
163
|
+
return float(cached["fetched_at"]), cached["index"]
|
|
164
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _fetch_remote_index() -> list[dict] | None:
|
|
169
|
+
"""Remote catalog entries, via the TTL cache. None means: use the bundled copy only."""
|
|
170
|
+
if os.environ.get(CATALOG_OFFLINE_ENV):
|
|
171
|
+
return None
|
|
172
|
+
cached = _read_index_cache()
|
|
173
|
+
if cached is not None and time.time() - cached[0] < CATALOG_INDEX_TTL:
|
|
174
|
+
return _index_to_entries(cached[1])
|
|
175
|
+
try:
|
|
176
|
+
index = _download_index()
|
|
177
|
+
except (httpx.HTTPError, ValueError) as exc:
|
|
178
|
+
_log(f"catalog index: fetch failed ({exc}); using {'cached' if cached else 'bundled'} catalog")
|
|
179
|
+
return _index_to_entries(cached[1]) if cached else None
|
|
180
|
+
entries = _index_to_entries(index)
|
|
181
|
+
if entries is None:
|
|
182
|
+
_log("catalog index: malformed document; using bundled catalog")
|
|
183
|
+
return None
|
|
184
|
+
try:
|
|
185
|
+
config.ensure_dirs()
|
|
186
|
+
config._atomic_write(_index_cache_path(), json.dumps({"fetched_at": time.time(), "index": index}))
|
|
187
|
+
except OSError as exc:
|
|
188
|
+
_log(f"catalog index: could not write cache: {exc}")
|
|
189
|
+
return entries
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _catalog_find(name: str) -> dict | None:
|
|
193
|
+
for entry in _load_catalog():
|
|
194
|
+
if name in (entry.get("_slug"), entry.get("name")):
|
|
195
|
+
return entry
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _catalog_to_profile(entry: dict) -> dict:
|
|
200
|
+
"""Map a catalog entry to a runtime profile (drop catalog-only metadata)."""
|
|
201
|
+
profile = {k: v for k, v in entry.items() if k not in _CATALOG_META_FIELDS and not k.startswith("_")}
|
|
202
|
+
profile["verify_ssl"] = True
|
|
203
|
+
return profile
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _auth_env_vars(auth: dict) -> list[str]:
|
|
207
|
+
"""Environment variables the user must set for this auth config."""
|
|
208
|
+
keys = {
|
|
209
|
+
"api-key": ("env_var",),
|
|
210
|
+
"bearer": ("token_env_var",),
|
|
211
|
+
"basic": ("username_env_var", "password_env_var"),
|
|
212
|
+
}.get(auth.get("type", "none"), ())
|
|
213
|
+
return [auth[k] for k in keys if auth.get(k)]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _auth_uses_login(auth: dict) -> bool:
|
|
217
|
+
kind = auth.get("type", "none")
|
|
218
|
+
return kind in ("oidc", "device") or (kind == "bearer" and bool(auth.get("token_endpoint")))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _auth_summary(auth: dict) -> str:
|
|
222
|
+
kind = auth.get("type", "none")
|
|
223
|
+
if _auth_uses_login(auth):
|
|
224
|
+
return f"{kind} → run 'login'"
|
|
225
|
+
env = _auth_env_vars(auth)
|
|
226
|
+
return f"{kind} → set {', '.join(env)}" if env else kind
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _render_catalog(entries: list[dict], title: str) -> None:
|
|
230
|
+
if not entries:
|
|
231
|
+
console.print("[dim]The catalog is empty.[/dim]")
|
|
232
|
+
return
|
|
233
|
+
table = Table(title=f"{title} ({len(entries)})")
|
|
234
|
+
table.add_column("Name", style="cyan")
|
|
235
|
+
table.add_column("Tier")
|
|
236
|
+
table.add_column("Domain")
|
|
237
|
+
table.add_column("Description", style="green")
|
|
238
|
+
for entry in sorted(entries, key=lambda e: (e.get("_tier", ""), e.get("_slug", ""))):
|
|
239
|
+
tier = entry.get("_tier", "community")
|
|
240
|
+
style = "bold green" if tier == "verified" else "yellow"
|
|
241
|
+
domain = "[bold blue]✓ dns[/bold blue]" if entry.get("domain_verified") is True else ""
|
|
242
|
+
table.add_row(entry.get("_slug", "?"), f"[{style}]{tier}[/{style}]", domain, str(entry.get("description", "")))
|
|
243
|
+
console.print(table)
|
|
244
|
+
console.print(
|
|
245
|
+
"[dim]Install one with: openapi-cli4ai catalog install <name>. "
|
|
246
|
+
"✓ dns = the API's domain owner published a DNS TXT record naming the maintainer.[/dim]"
|
|
247
|
+
)
|