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.
- cli_to_py-0.1.0/.github/workflows/ci.yml +37 -0
- cli_to_py-0.1.0/.github/workflows/pages.yml +59 -0
- cli_to_py-0.1.0/.gitignore +8 -0
- cli_to_py-0.1.0/LICENSE +21 -0
- cli_to_py-0.1.0/Makefile +58 -0
- cli_to_py-0.1.0/PKG-INFO +135 -0
- cli_to_py-0.1.0/README.md +112 -0
- cli_to_py-0.1.0/cli_to_py/__init__.py +113 -0
- cli_to_py-0.1.0/cli_to_py/_api_base.py +112 -0
- cli_to_py-0.1.0/cli_to_py/api.py +159 -0
- cli_to_py-0.1.0/cli_to_py/best_help.py +45 -0
- cli_to_py-0.1.0/cli_to_py/cache.py +165 -0
- cli_to_py-0.1.0/cli_to_py/case.py +11 -0
- cli_to_py-0.1.0/cli_to_py/cli.py +99 -0
- cli_to_py-0.1.0/cli_to_py/command_future.py +55 -0
- cli_to_py-0.1.0/cli_to_py/command_string.py +19 -0
- cli_to_py-0.1.0/cli_to_py/constants.py +12 -0
- cli_to_py-0.1.0/cli_to_py/convert.py +102 -0
- cli_to_py-0.1.0/cli_to_py/env_utils.py +46 -0
- cli_to_py-0.1.0/cli_to_py/exec.py +432 -0
- cli_to_py-0.1.0/cli_to_py/generate.py +306 -0
- cli_to_py-0.1.0/cli_to_py/load_schema.py +45 -0
- cli_to_py-0.1.0/cli_to_py/options_to_args.py +69 -0
- cli_to_py-0.1.0/cli_to_py/parse_help.py +295 -0
- cli_to_py-0.1.0/cli_to_py/parse_subcommands.py +186 -0
- cli_to_py-0.1.0/cli_to_py/run_config.py +74 -0
- cli_to_py-0.1.0/cli_to_py/schema.py +80 -0
- cli_to_py-0.1.0/cli_to_py/script.py +114 -0
- cli_to_py-0.1.0/cli_to_py/sync_api.py +136 -0
- cli_to_py-0.1.0/cli_to_py/sync_exec.py +158 -0
- cli_to_py-0.1.0/cli_to_py/validate.py +149 -0
- cli_to_py-0.1.0/docs/api-reference/public-api.md +34 -0
- cli_to_py-0.1.0/docs/development/release-checklist.md +26 -0
- cli_to_py-0.1.0/docs/getting-started/installation.md +33 -0
- cli_to_py-0.1.0/docs/getting-started/quickstart.md +69 -0
- cli_to_py-0.1.0/docs/index.md +47 -0
- cli_to_py-0.1.0/docs/user-guide/code-generation.md +35 -0
- cli_to_py-0.1.0/docs/user-guide/parser-limits.md +28 -0
- cli_to_py-0.1.0/docs/user-guide/runtime-control.md +60 -0
- cli_to_py-0.1.0/docs/user-guide/validation.md +40 -0
- cli_to_py-0.1.0/mkdocs.yml +45 -0
- cli_to_py-0.1.0/notes.md +104 -0
- cli_to_py-0.1.0/pyproject.toml +44 -0
- cli_to_py-0.1.0/smoke_test.py +271 -0
- cli_to_py-0.1.0/tests/__init__.py +0 -0
- cli_to_py-0.1.0/tests/conftest.py +8 -0
- cli_to_py-0.1.0/tests/fixtures.py +88 -0
- cli_to_py-0.1.0/tests/integration/__init__.py +0 -0
- cli_to_py-0.1.0/tests/integration/test_cache_roundtrip.py +65 -0
- cli_to_py-0.1.0/tests/integration/test_cli_entry.py +80 -0
- cli_to_py-0.1.0/tests/integration/test_cli_subprocess.py +114 -0
- cli_to_py-0.1.0/tests/integration/test_exec_features.py +184 -0
- cli_to_py-0.1.0/tests/integration/test_real_binaries.py +112 -0
- cli_to_py-0.1.0/tests/integration/test_spawn_stream.py +33 -0
- cli_to_py-0.1.0/tests/integration/test_stdio_inherit.py +43 -0
- cli_to_py-0.1.0/tests/unit/__init__.py +0 -0
- cli_to_py-0.1.0/tests/unit/test_api.py +161 -0
- cli_to_py-0.1.0/tests/unit/test_best_help.py +32 -0
- cli_to_py-0.1.0/tests/unit/test_cache.py +116 -0
- cli_to_py-0.1.0/tests/unit/test_case.py +25 -0
- cli_to_py-0.1.0/tests/unit/test_color_detect.py +72 -0
- cli_to_py-0.1.0/tests/unit/test_command_future.py +54 -0
- cli_to_py-0.1.0/tests/unit/test_command_string.py +45 -0
- cli_to_py-0.1.0/tests/unit/test_generate.py +86 -0
- cli_to_py-0.1.0/tests/unit/test_options_to_args.py +82 -0
- cli_to_py-0.1.0/tests/unit/test_parse_help.py +185 -0
- cli_to_py-0.1.0/tests/unit/test_run_config.py +87 -0
- cli_to_py-0.1.0/tests/unit/test_script.py +57 -0
- cli_to_py-0.1.0/tests/unit/test_validate.py +134 -0
- 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
|
cli_to_py-0.1.0/LICENSE
ADDED
|
@@ -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.
|
cli_to_py-0.1.0/Makefile
ADDED
|
@@ -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/*
|
cli_to_py-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/cli-to-py/)
|
|
27
|
+
[](https://pypi.org/project/cli-to-py/)
|
|
28
|
+
[](https://github.com/oneryalcin/cli-to-py/actions/workflows/ci.yml)
|
|
29
|
+
[](https://github.com/oneryalcin/cli-to-py/actions/workflows/pages.yml)
|
|
30
|
+
[](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
|
+
[](https://pypi.org/project/cli-to-py/)
|
|
4
|
+
[](https://pypi.org/project/cli-to-py/)
|
|
5
|
+
[](https://github.com/oneryalcin/cli-to-py/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/oneryalcin/cli-to-py/actions/workflows/pages.yml)
|
|
7
|
+
[](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
|