cli-to-py 0.1.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.
Files changed (70) hide show
  1. cli_to_py-0.1.0/.github/workflows/ci.yml +37 -0
  2. cli_to_py-0.1.0/.github/workflows/pages.yml +59 -0
  3. cli_to_py-0.1.0/.gitignore +8 -0
  4. cli_to_py-0.1.0/LICENSE +21 -0
  5. cli_to_py-0.1.0/Makefile +58 -0
  6. cli_to_py-0.1.0/PKG-INFO +135 -0
  7. cli_to_py-0.1.0/README.md +112 -0
  8. cli_to_py-0.1.0/cli_to_py/__init__.py +113 -0
  9. cli_to_py-0.1.0/cli_to_py/_api_base.py +112 -0
  10. cli_to_py-0.1.0/cli_to_py/api.py +159 -0
  11. cli_to_py-0.1.0/cli_to_py/best_help.py +45 -0
  12. cli_to_py-0.1.0/cli_to_py/cache.py +165 -0
  13. cli_to_py-0.1.0/cli_to_py/case.py +11 -0
  14. cli_to_py-0.1.0/cli_to_py/cli.py +99 -0
  15. cli_to_py-0.1.0/cli_to_py/command_future.py +55 -0
  16. cli_to_py-0.1.0/cli_to_py/command_string.py +19 -0
  17. cli_to_py-0.1.0/cli_to_py/constants.py +12 -0
  18. cli_to_py-0.1.0/cli_to_py/convert.py +102 -0
  19. cli_to_py-0.1.0/cli_to_py/env_utils.py +46 -0
  20. cli_to_py-0.1.0/cli_to_py/exec.py +432 -0
  21. cli_to_py-0.1.0/cli_to_py/generate.py +306 -0
  22. cli_to_py-0.1.0/cli_to_py/load_schema.py +45 -0
  23. cli_to_py-0.1.0/cli_to_py/options_to_args.py +69 -0
  24. cli_to_py-0.1.0/cli_to_py/parse_help.py +295 -0
  25. cli_to_py-0.1.0/cli_to_py/parse_subcommands.py +186 -0
  26. cli_to_py-0.1.0/cli_to_py/run_config.py +74 -0
  27. cli_to_py-0.1.0/cli_to_py/schema.py +80 -0
  28. cli_to_py-0.1.0/cli_to_py/script.py +114 -0
  29. cli_to_py-0.1.0/cli_to_py/sync_api.py +136 -0
  30. cli_to_py-0.1.0/cli_to_py/sync_exec.py +158 -0
  31. cli_to_py-0.1.0/cli_to_py/validate.py +149 -0
  32. cli_to_py-0.1.0/docs/api-reference/public-api.md +34 -0
  33. cli_to_py-0.1.0/docs/development/release-checklist.md +26 -0
  34. cli_to_py-0.1.0/docs/getting-started/installation.md +33 -0
  35. cli_to_py-0.1.0/docs/getting-started/quickstart.md +69 -0
  36. cli_to_py-0.1.0/docs/index.md +47 -0
  37. cli_to_py-0.1.0/docs/user-guide/code-generation.md +35 -0
  38. cli_to_py-0.1.0/docs/user-guide/parser-limits.md +28 -0
  39. cli_to_py-0.1.0/docs/user-guide/runtime-control.md +60 -0
  40. cli_to_py-0.1.0/docs/user-guide/validation.md +40 -0
  41. cli_to_py-0.1.0/mkdocs.yml +45 -0
  42. cli_to_py-0.1.0/notes.md +104 -0
  43. cli_to_py-0.1.0/pyproject.toml +44 -0
  44. cli_to_py-0.1.0/smoke_test.py +271 -0
  45. cli_to_py-0.1.0/tests/__init__.py +0 -0
  46. cli_to_py-0.1.0/tests/conftest.py +8 -0
  47. cli_to_py-0.1.0/tests/fixtures.py +88 -0
  48. cli_to_py-0.1.0/tests/integration/__init__.py +0 -0
  49. cli_to_py-0.1.0/tests/integration/test_cache_roundtrip.py +65 -0
  50. cli_to_py-0.1.0/tests/integration/test_cli_entry.py +80 -0
  51. cli_to_py-0.1.0/tests/integration/test_cli_subprocess.py +114 -0
  52. cli_to_py-0.1.0/tests/integration/test_exec_features.py +184 -0
  53. cli_to_py-0.1.0/tests/integration/test_real_binaries.py +112 -0
  54. cli_to_py-0.1.0/tests/integration/test_spawn_stream.py +33 -0
  55. cli_to_py-0.1.0/tests/integration/test_stdio_inherit.py +43 -0
  56. cli_to_py-0.1.0/tests/unit/__init__.py +0 -0
  57. cli_to_py-0.1.0/tests/unit/test_api.py +161 -0
  58. cli_to_py-0.1.0/tests/unit/test_best_help.py +32 -0
  59. cli_to_py-0.1.0/tests/unit/test_cache.py +116 -0
  60. cli_to_py-0.1.0/tests/unit/test_case.py +25 -0
  61. cli_to_py-0.1.0/tests/unit/test_color_detect.py +72 -0
  62. cli_to_py-0.1.0/tests/unit/test_command_future.py +54 -0
  63. cli_to_py-0.1.0/tests/unit/test_command_string.py +45 -0
  64. cli_to_py-0.1.0/tests/unit/test_generate.py +86 -0
  65. cli_to_py-0.1.0/tests/unit/test_options_to_args.py +82 -0
  66. cli_to_py-0.1.0/tests/unit/test_parse_help.py +185 -0
  67. cli_to_py-0.1.0/tests/unit/test_run_config.py +87 -0
  68. cli_to_py-0.1.0/tests/unit/test_script.py +57 -0
  69. cli_to_py-0.1.0/tests/unit/test_validate.py +134 -0
  70. cli_to_py-0.1.0/uv.lock +104 -0
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ jobs:
10
+ test:
11
+ name: Python ${{ matrix.python-version }}
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version:
17
+ - "3.11"
18
+ - "3.12"
19
+ - "3.13"
20
+ - "3.14"
21
+
22
+ steps:
23
+ - name: Check out repository
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Set up Python
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+
31
+ - name: Set up uv
32
+ uses: astral-sh/setup-uv@v5
33
+ with:
34
+ enable-cache: true
35
+
36
+ - name: Run tests and build package
37
+ run: make ci
@@ -0,0 +1,59 @@
1
+ name: Docs
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+ pages: write
13
+ id-token: write
14
+
15
+ concurrency:
16
+ group: pages
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ build:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - name: Check out repository
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Set up Python
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: "3.12"
30
+
31
+ - name: Set up uv
32
+ uses: astral-sh/setup-uv@v5
33
+ with:
34
+ enable-cache: true
35
+
36
+ - name: Build docs
37
+ run: make docs
38
+
39
+ - name: Configure Pages
40
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
41
+ uses: actions/configure-pages@v5
42
+
43
+ - name: Upload Pages artifact
44
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
45
+ uses: actions/upload-pages-artifact@v3
46
+ with:
47
+ path: site
48
+
49
+ deploy:
50
+ needs: build
51
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
52
+ runs-on: ubuntu-latest
53
+ environment:
54
+ name: github-pages
55
+ url: ${{ steps.deployment.outputs.page_url }}
56
+ steps:
57
+ - name: Deploy to GitHub Pages
58
+ id: deployment
59
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,8 @@
1
+ scratch/
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ dist/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .coverage
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mehmet Oner Yalcin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ UV ?= uv --no-config
2
+ PYTEST ?= python -m pytest
3
+ PYPI_CHECK_URL ?= https://pypi.org/simple/
4
+
5
+ .PHONY: help test test-unit test-integration build clean ci smoke docs docs-serve publish-check publish
6
+
7
+ help:
8
+ @printf '%s\n' \
9
+ 'Targets:' \
10
+ ' make test Run unit and integration tests' \
11
+ ' make test-unit Run unit tests only' \
12
+ ' make test-integration Run integration tests only' \
13
+ ' make build Build sdist and wheel' \
14
+ ' make ci Run CI-equivalent checks except matrix Python' \
15
+ ' make smoke Run local smoke script (not used in CI)' \
16
+ ' make docs Build MkDocs documentation' \
17
+ ' make docs-serve Serve MkDocs documentation locally' \
18
+ ' make clean Remove local generated artifacts' \
19
+ ' make publish-check Dry-run publish built artifacts' \
20
+ ' make publish Publish built artifacts to PyPI'
21
+
22
+ test:
23
+ $(UV) run --locked --extra dev $(PYTEST) -q
24
+
25
+ test-unit:
26
+ $(UV) run --locked --extra dev $(PYTEST) tests/unit -q
27
+
28
+ test-integration:
29
+ $(UV) run --locked --extra dev $(PYTEST) tests/integration -q
30
+
31
+ build:
32
+ $(UV) build
33
+
34
+ ci: test build
35
+
36
+ smoke:
37
+ $(UV) run --locked python smoke_test.py
38
+
39
+ docs:
40
+ $(UV) run --with mkdocs-material mkdocs build --strict
41
+
42
+ docs-serve:
43
+ $(UV) run --with mkdocs-material mkdocs serve
44
+
45
+ clean:
46
+ rm -rf .pytest_cache dist site *.egg-info
47
+ find . -type d -name __pycache__ -prune -exec rm -rf {} +
48
+
49
+ publish-check: build
50
+ @if [ -n "$$PYPI_TOKEN" ]; then \
51
+ UV_PUBLISH_TOKEN="$$PYPI_TOKEN" $(UV) publish --dry-run --check-url $(PYPI_CHECK_URL) dist/*; \
52
+ else \
53
+ $(UV) publish --dry-run --check-url $(PYPI_CHECK_URL) dist/*; \
54
+ fi
55
+
56
+ publish: build
57
+ @test -n "$(PYPI_TOKEN)" || (printf '%s\n' 'PYPI_TOKEN is required' >&2; exit 1)
58
+ @UV_PUBLISH_TOKEN="$(PYPI_TOKEN)" $(UV) publish --check-url $(PYPI_CHECK_URL) dist/*
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-to-py
3
+ Version: 0.1.0
4
+ Summary: Turn any CLI into a Python API, automatically.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: agent,cli,introspection,subprocess,wrapper
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Shells
18
+ Requires-Python: >=3.11
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
21
+ Requires-Dist: pytest>=7.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # cli-to-py
25
+
26
+ [![PyPI version](https://img.shields.io/pypi/v/cli-to-py)](https://pypi.org/project/cli-to-py/)
27
+ [![Python versions](https://img.shields.io/pypi/pyversions/cli-to-py)](https://pypi.org/project/cli-to-py/)
28
+ [![CI](https://github.com/oneryalcin/cli-to-py/actions/workflows/ci.yml/badge.svg)](https://github.com/oneryalcin/cli-to-py/actions/workflows/ci.yml)
29
+ [![Docs](https://github.com/oneryalcin/cli-to-py/actions/workflows/pages.yml/badge.svg)](https://github.com/oneryalcin/cli-to-py/actions/workflows/pages.yml)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
31
+
32
+ Turn CLI binaries into Python APIs.
33
+
34
+ `cli-to-py` reads a command's help output, builds a callable Python wrapper, and
35
+ lets you run subcommands with keyword arguments instead of hand-built shell
36
+ strings.
37
+
38
+ Full documentation: <https://oneryalcin.github.io/cli-to-py/>
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install cli-to-py
44
+ ```
45
+
46
+ or:
47
+
48
+ ```bash
49
+ uv add cli-to-py
50
+ ```
51
+
52
+ Requires Python 3.11 or newer.
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ import asyncio
58
+ from cli_to_py import convert
59
+
60
+ async def main():
61
+ git = await convert("git")
62
+
63
+ result = await git.status(short=True)
64
+ print(result.stdout)
65
+
66
+ branch = await git.branch(show_current=True).text()
67
+ changed = await git("diff", name_only=True, _=["HEAD~1"]).lines()
68
+
69
+ print(branch)
70
+ print(changed)
71
+
72
+ asyncio.run(main())
73
+ ```
74
+
75
+ Flags use Python names and are converted to CLI flags:
76
+
77
+ ```python
78
+ await git.commit(message="fix", all=True)
79
+ # runs: git commit --message fix --all
80
+ ```
81
+
82
+ Use `_` for positional arguments:
83
+
84
+ ```python
85
+ await git("diff", name_only=True, _=["HEAD~1"])
86
+ # runs: git diff --name-only HEAD~1
87
+ ```
88
+
89
+ ## Why Use It
90
+
91
+ - Convert CLIs into async Python APIs with no runtime dependencies.
92
+ - Validate flags and arguments before spawning a subprocess.
93
+ - Keep subprocess output ergonomic with `.text()`, `.lines()`, and `.json()`.
94
+ - Stream output, inherit stdio, set timeouts, pass env/cwd, and cancel work.
95
+ - Generate standalone wrapper modules for tools you want to check into a project.
96
+
97
+ ## Documentation
98
+
99
+ - [Installation](https://oneryalcin.github.io/cli-to-py/getting-started/installation/)
100
+ - [Quick start](https://oneryalcin.github.io/cli-to-py/getting-started/quickstart/)
101
+ - [Validation](https://oneryalcin.github.io/cli-to-py/user-guide/validation/)
102
+ - [Runtime control](https://oneryalcin.github.io/cli-to-py/user-guide/runtime-control/)
103
+ - [Code generation](https://oneryalcin.github.io/cli-to-py/user-guide/code-generation/)
104
+ - [Parser limits](https://oneryalcin.github.io/cli-to-py/user-guide/parser-limits/)
105
+ - [Public API](https://oneryalcin.github.io/cli-to-py/api-reference/public-api/)
106
+
107
+ ## CLI Wrapper Generation
108
+
109
+ Generate a dependency-free Python wrapper from a CLI:
110
+
111
+ ```bash
112
+ cli-to-py git -o git_wrapper.py
113
+ ```
114
+
115
+ The generated `.py` module and `.pyi` stub can be committed to another project
116
+ without depending on `cli-to-py` at runtime.
117
+
118
+ ## Development
119
+
120
+ ```bash
121
+ make test # unit and integration tests
122
+ make ci # full CI path, including package build
123
+ make docs # strict MkDocs build
124
+ make build # source distribution and wheel
125
+ ```
126
+
127
+ See the [release checklist](https://oneryalcin.github.io/cli-to-py/development/release-checklist/)
128
+ for publishing steps.
129
+
130
+ ## Status
131
+
132
+ This project parses common `--help` formats pragmatically. It is useful for many
133
+ CLIs, but it is not a formal parser for every help style. See
134
+ [parser limits](https://oneryalcin.github.io/cli-to-py/user-guide/parser-limits/)
135
+ before relying on generated wrappers for unusual CLIs.
@@ -0,0 +1,112 @@
1
+ # cli-to-py
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/cli-to-py)](https://pypi.org/project/cli-to-py/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/cli-to-py)](https://pypi.org/project/cli-to-py/)
5
+ [![CI](https://github.com/oneryalcin/cli-to-py/actions/workflows/ci.yml/badge.svg)](https://github.com/oneryalcin/cli-to-py/actions/workflows/ci.yml)
6
+ [![Docs](https://github.com/oneryalcin/cli-to-py/actions/workflows/pages.yml/badge.svg)](https://github.com/oneryalcin/cli-to-py/actions/workflows/pages.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ Turn CLI binaries into Python APIs.
10
+
11
+ `cli-to-py` reads a command's help output, builds a callable Python wrapper, and
12
+ lets you run subcommands with keyword arguments instead of hand-built shell
13
+ strings.
14
+
15
+ Full documentation: <https://oneryalcin.github.io/cli-to-py/>
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install cli-to-py
21
+ ```
22
+
23
+ or:
24
+
25
+ ```bash
26
+ uv add cli-to-py
27
+ ```
28
+
29
+ Requires Python 3.11 or newer.
30
+
31
+ ## Quick Start
32
+
33
+ ```python
34
+ import asyncio
35
+ from cli_to_py import convert
36
+
37
+ async def main():
38
+ git = await convert("git")
39
+
40
+ result = await git.status(short=True)
41
+ print(result.stdout)
42
+
43
+ branch = await git.branch(show_current=True).text()
44
+ changed = await git("diff", name_only=True, _=["HEAD~1"]).lines()
45
+
46
+ print(branch)
47
+ print(changed)
48
+
49
+ asyncio.run(main())
50
+ ```
51
+
52
+ Flags use Python names and are converted to CLI flags:
53
+
54
+ ```python
55
+ await git.commit(message="fix", all=True)
56
+ # runs: git commit --message fix --all
57
+ ```
58
+
59
+ Use `_` for positional arguments:
60
+
61
+ ```python
62
+ await git("diff", name_only=True, _=["HEAD~1"])
63
+ # runs: git diff --name-only HEAD~1
64
+ ```
65
+
66
+ ## Why Use It
67
+
68
+ - Convert CLIs into async Python APIs with no runtime dependencies.
69
+ - Validate flags and arguments before spawning a subprocess.
70
+ - Keep subprocess output ergonomic with `.text()`, `.lines()`, and `.json()`.
71
+ - Stream output, inherit stdio, set timeouts, pass env/cwd, and cancel work.
72
+ - Generate standalone wrapper modules for tools you want to check into a project.
73
+
74
+ ## Documentation
75
+
76
+ - [Installation](https://oneryalcin.github.io/cli-to-py/getting-started/installation/)
77
+ - [Quick start](https://oneryalcin.github.io/cli-to-py/getting-started/quickstart/)
78
+ - [Validation](https://oneryalcin.github.io/cli-to-py/user-guide/validation/)
79
+ - [Runtime control](https://oneryalcin.github.io/cli-to-py/user-guide/runtime-control/)
80
+ - [Code generation](https://oneryalcin.github.io/cli-to-py/user-guide/code-generation/)
81
+ - [Parser limits](https://oneryalcin.github.io/cli-to-py/user-guide/parser-limits/)
82
+ - [Public API](https://oneryalcin.github.io/cli-to-py/api-reference/public-api/)
83
+
84
+ ## CLI Wrapper Generation
85
+
86
+ Generate a dependency-free Python wrapper from a CLI:
87
+
88
+ ```bash
89
+ cli-to-py git -o git_wrapper.py
90
+ ```
91
+
92
+ The generated `.py` module and `.pyi` stub can be committed to another project
93
+ without depending on `cli-to-py` at runtime.
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ make test # unit and integration tests
99
+ make ci # full CI path, including package build
100
+ make docs # strict MkDocs build
101
+ make build # source distribution and wheel
102
+ ```
103
+
104
+ See the [release checklist](https://oneryalcin.github.io/cli-to-py/development/release-checklist/)
105
+ for publishing steps.
106
+
107
+ ## Status
108
+
109
+ This project parses common `--help` formats pragmatically. It is useful for many
110
+ CLIs, but it is not a formal parser for every help style. See
111
+ [parser limits](https://oneryalcin.github.io/cli-to-py/user-guide/parser-limits/)
112
+ before relying on generated wrappers for unusual CLIs.
@@ -0,0 +1,113 @@
1
+ """cli_to_py — Turn any CLI into a Python API, automatically.
2
+
3
+ Public API:
4
+
5
+ from cli_to_py import convert, convert_sync, from_help_text
6
+
7
+ # Async
8
+ git = await convert("git")
9
+ result = await git.status(short=True)
10
+ branch = await git.branch(show_current=True).text()
11
+ errors = git.validate("commit", massage="x") # returns [ValidationError]
12
+
13
+ # Sync
14
+ git = convert_sync("git")
15
+ result = git.status(short=True)
16
+
17
+ # Static help text
18
+ api = from_help_text("my-tool", help_text)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from .api import CliApi
24
+ from .cache import cache_dir, clear_cache, load_cached_schema, save_cached_schema
25
+ from .command_future import CommandFuture
26
+ from .command_string import to_command_string
27
+ from .convert import convert, convert_sync, from_help_text, from_help_text_sync
28
+ from .exec import (
29
+ BinaryNotFoundError,
30
+ CommandAborted,
31
+ CommandProcess,
32
+ CommandTimeout,
33
+ run_command,
34
+ run_for_help,
35
+ spawn_command,
36
+ )
37
+ from .generate import generate_json, generate_stub, generate_wrapper
38
+ from .load_schema import load_schema, load_schema_sync
39
+ from .options_to_args import options_to_args
40
+ from .parse_help import parse_help_text, strip_ansi
41
+ from .parse_subcommands import enrich_subcommands, parse_subcommand_help
42
+ from .run_config import RunConfig
43
+ from .schema import (
44
+ CliSchema,
45
+ CommandResult,
46
+ ParsedCommand,
47
+ ParsedFlag,
48
+ ParsedPositionalArg,
49
+ ParsedSubcommand,
50
+ )
51
+ from .script import Script, script
52
+ from .sync_api import SyncCliApi
53
+ from .sync_exec import run_command_sync, run_for_help_sync
54
+ from .validate import ValidationError, validate_options
55
+
56
+ __version__ = "0.1.0"
57
+
58
+ __all__ = [
59
+ # version
60
+ "__version__",
61
+ # entry points
62
+ "convert",
63
+ "convert_sync",
64
+ "from_help_text",
65
+ "from_help_text_sync",
66
+ "load_schema",
67
+ "load_schema_sync",
68
+ # api objects
69
+ "CliApi",
70
+ "SyncCliApi",
71
+ "CommandFuture",
72
+ "CommandProcess",
73
+ # schema types
74
+ "CliSchema",
75
+ "ParsedCommand",
76
+ "ParsedSubcommand",
77
+ "ParsedFlag",
78
+ "ParsedPositionalArg",
79
+ "CommandResult",
80
+ # config
81
+ "RunConfig",
82
+ # validation
83
+ "validate_options",
84
+ "ValidationError",
85
+ # execution
86
+ "run_command",
87
+ "run_command_sync",
88
+ "run_for_help",
89
+ "run_for_help_sync",
90
+ "spawn_command",
91
+ "CommandTimeout",
92
+ "CommandAborted",
93
+ "BinaryNotFoundError",
94
+ # parsing
95
+ "parse_help_text",
96
+ "strip_ansi",
97
+ "parse_subcommand_help",
98
+ "enrich_subcommands",
99
+ "options_to_args",
100
+ "to_command_string",
101
+ # codegen
102
+ "generate_wrapper",
103
+ "generate_stub",
104
+ "generate_json",
105
+ # script chaining
106
+ "Script",
107
+ "script",
108
+ # cache
109
+ "cache_dir",
110
+ "load_cached_schema",
111
+ "save_cached_schema",
112
+ "clear_cache",
113
+ ]
@@ -0,0 +1,112 @@
1
+ """Shared plumbing for CliApi (async) and SyncCliApi (sync).
2
+
3
+ These helpers were duplicated across api.py and sync_api.py byte-for-byte.
4
+ Extracted here so a change to alias resolution or kwargs handling lands
5
+ in exactly one place.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from .case import kebab_to_snake, snake_to_kebab
13
+ from .run_config import RunConfig
14
+ from .schema import CliSchema, ParsedSubcommand
15
+
16
+
17
+ class _BaseCliApi:
18
+ """Base class holding dispatch/resolution plumbing shared by CliApi and SyncCliApi.
19
+
20
+ Subclasses own `__call__` and `__getattr__` semantics (async vs sync) but
21
+ share alias resolution, equals-flag collection, config merging, and the
22
+ `_config` kwarg extraction convention.
23
+ """
24
+
25
+ binary_name: str
26
+ schema: CliSchema
27
+ _default_config: RunConfig
28
+ _equals_flags: set[str]
29
+
30
+ def __init__(
31
+ self,
32
+ binary_name: str,
33
+ schema: CliSchema,
34
+ default_config: RunConfig | None = None,
35
+ ):
36
+ self.binary_name = binary_name
37
+ self.schema = schema
38
+ self._default_config = default_config or RunConfig()
39
+ self._equals_flags = {
40
+ kebab_to_snake(f.long_name) for f in schema.command.flags if f.uses_equals
41
+ }
42
+
43
+ # --- resolution helpers
44
+
45
+ def _resolve_alias(self, name: str) -> str:
46
+ normalized = snake_to_kebab(name)
47
+ for sub in self.schema.command.subcommands:
48
+ if (
49
+ sub.name == name
50
+ or sub.name == normalized
51
+ or (sub.aliases and (name in sub.aliases or normalized in sub.aliases))
52
+ ):
53
+ return sub.name
54
+ return name
55
+
56
+ def _find_subcommand(self, name: str) -> ParsedSubcommand | None:
57
+ normalized = snake_to_kebab(name)
58
+ for sub in self.schema.command.subcommands:
59
+ if (
60
+ sub.name == name
61
+ or sub.name == normalized
62
+ or (sub.aliases and (name in sub.aliases or normalized in sub.aliases))
63
+ ):
64
+ return sub
65
+ return None
66
+
67
+ def _equals_for(self, resolved_sub: str) -> set[str]:
68
+ sub = self._find_subcommand(resolved_sub)
69
+ if sub is None or not sub.flags:
70
+ return self._equals_flags
71
+ sub_equals = {kebab_to_snake(f.long_name) for f in sub.flags if f.uses_equals}
72
+ return self._equals_flags | sub_equals
73
+
74
+ def _split_kwargs(
75
+ self, kwargs: dict[str, Any]
76
+ ) -> tuple[dict[str, Any], RunConfig | None]:
77
+ """Extract an optional `_config` kwarg (a RunConfig) from user options.
78
+
79
+ Also guards against the common typo where a user passes
80
+ `config=RunConfig(...)` instead of `_config=RunConfig(...)` — without
81
+ the underscore it would serialize to argv as `--config <repr>`, which
82
+ is never what the user intended.
83
+ """
84
+ config = kwargs.pop("_config", None)
85
+ if config is not None and not isinstance(config, RunConfig):
86
+ raise TypeError("_config must be a RunConfig instance")
87
+ # Stray RunConfig values under non-underscore keys are almost always typos.
88
+ for key, value in kwargs.items():
89
+ if isinstance(value, RunConfig):
90
+ raise TypeError(
91
+ f"kwarg {key!r} holds a RunConfig — did you mean `_config=...`?"
92
+ )
93
+ return kwargs, config
94
+
95
+ def _merged_config(self, per_call: RunConfig | None) -> RunConfig:
96
+ return self._default_config.merge(per_call)
97
+
98
+ # --- introspection helpers
99
+
100
+ def _subcommand_names(self) -> list[str]:
101
+ """Return known subcommand names and aliases, without duplicates."""
102
+ names: list[str] = []
103
+ seen: set[str] = set()
104
+ for sub in self.schema.command.subcommands:
105
+ if sub.name not in seen:
106
+ names.append(sub.name)
107
+ seen.add(sub.name)
108
+ for alias in sub.aliases or ():
109
+ if alias not in seen:
110
+ names.append(alias)
111
+ seen.add(alias)
112
+ return names