panda-cli 0.1.3__tar.gz → 0.3.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.
- panda_cli-0.3.0/.gitignore +162 -0
- panda_cli-0.3.0/PKG-INFO +199 -0
- panda_cli-0.3.0/README.md +180 -0
- panda_cli-0.3.0/examples/web.py +47 -0
- panda_cli-0.3.0/makefile +40 -0
- panda_cli-0.3.0/pyproject.toml +83 -0
- panda_cli-0.3.0/src/panda_cli/__init__.py +15 -0
- panda_cli-0.3.0/src/panda_cli/annotations.py +79 -0
- panda_cli-0.3.0/src/panda_cli/backends/__init__.py +5 -0
- panda_cli-0.3.0/src/panda_cli/backends/click_backend.py +202 -0
- panda_cli-0.3.0/src/panda_cli/base.py +151 -0
- panda_cli-0.3.0/src/panda_cli/errors.py +38 -0
- panda_cli-0.3.0/src/panda_cli/introspect.py +179 -0
- panda_cli-0.3.0/src/panda_cli/option.py +142 -0
- panda_cli-0.3.0/src/panda_cli/runtime.py +67 -0
- panda_cli-0.3.0/src/panda_cli/spec.py +144 -0
- {panda_cli-0.1.3 → panda_cli-0.3.0}/uv.lock +152 -1
- panda_cli-0.1.3/PKG-INFO +0 -8
- panda_cli-0.1.3/README.md +0 -0
- panda_cli-0.1.3/_temp/__init__.py +0 -6
- panda_cli-0.1.3/_temp/base_option.py +0 -99
- panda_cli-0.1.3/_temp/base_tree.py +0 -127
- panda_cli-0.1.3/_temp/option_contstructor.py +0 -56
- panda_cli-0.1.3/_temp/tools.py +0 -54
- panda_cli-0.1.3/pyproject.toml +0 -28
- panda_cli-0.1.3/src/panda_cli/__init__.py +0 -6
- panda_cli-0.1.3/src/panda_cli/base.py +0 -176
- panda_cli-0.1.3/src/panda_cli/completion.py +0 -0
- panda_cli-0.1.3/src/panda_cli/option.py +0 -180
- panda_cli-0.1.3/src/panda_cli/parameters.py +0 -28
- panda_cli-0.1.3/src/panda_cli/py.typed +0 -0
- panda_cli-0.1.3/tests/test.py +0 -34
- {panda_cli-0.1.3 → panda_cli-0.3.0}/.python-version +0 -0
- {panda_cli-0.1.3/_temp → panda_cli-0.3.0/src/panda_cli}/py.typed +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
*.log
|
|
2
|
+
*.pot
|
|
3
|
+
*.pyc
|
|
4
|
+
__pycache__/
|
|
5
|
+
__pycache__
|
|
6
|
+
local_settings.py
|
|
7
|
+
db.sqlite3
|
|
8
|
+
db.sqlite3-journal
|
|
9
|
+
media
|
|
10
|
+
.env
|
|
11
|
+
*.py[cod]
|
|
12
|
+
*$py.class
|
|
13
|
+
|
|
14
|
+
*.so
|
|
15
|
+
.idea/
|
|
16
|
+
.DS_Store
|
|
17
|
+
.Python
|
|
18
|
+
build/
|
|
19
|
+
develop-eggs/
|
|
20
|
+
dist/
|
|
21
|
+
downloads/
|
|
22
|
+
eggs/
|
|
23
|
+
.eggs/
|
|
24
|
+
lib/
|
|
25
|
+
lib64/
|
|
26
|
+
parts/
|
|
27
|
+
sdist/
|
|
28
|
+
var/
|
|
29
|
+
wheels/
|
|
30
|
+
share/python-wheels/
|
|
31
|
+
*.egg-info/
|
|
32
|
+
.installed.cfg
|
|
33
|
+
*.egg
|
|
34
|
+
MANIFEST
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
*.manifest
|
|
38
|
+
*.spec
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
pip-log.txt
|
|
42
|
+
pip-delete-this-directory.txt
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
htmlcov/
|
|
46
|
+
.tox/
|
|
47
|
+
.nox/
|
|
48
|
+
.coverage
|
|
49
|
+
.coverage.*
|
|
50
|
+
.cache
|
|
51
|
+
nosetests.xml
|
|
52
|
+
coverage.xml
|
|
53
|
+
*.cover
|
|
54
|
+
*.py,cover
|
|
55
|
+
.hypothesis/
|
|
56
|
+
.pytest_cache/
|
|
57
|
+
cover/
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
*.mo
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
instance/
|
|
65
|
+
.webassets-cache
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
.scrapy
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
docs/_build/
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
.pybuilder/
|
|
75
|
+
target/
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
.ipynb_checkpoints
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
profile_default/
|
|
82
|
+
ipython_config.py
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
.pdm.toml
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__pypackages__/
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
celerybeat-schedule
|
|
92
|
+
celerybeat.pid
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
*.sage.py
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
.venv
|
|
100
|
+
venv/
|
|
101
|
+
env.bak/
|
|
102
|
+
venv.bak/
|
|
103
|
+
|
|
104
|
+
.spyderproject
|
|
105
|
+
.spyproject
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
.ropeproject
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/site
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
.mypy_cache/
|
|
115
|
+
.dmypy.json
|
|
116
|
+
dmypy.json
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
.pyre/
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
.pytype/
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
cython_debug/
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
.idea/**/workspace.xml
|
|
129
|
+
.idea/**/tasks.xml
|
|
130
|
+
.idea/**/usage.statistics.xml
|
|
131
|
+
.idea/**/dictionaries
|
|
132
|
+
.idea/**/shelf
|
|
133
|
+
.idea
|
|
134
|
+
.DS_Store
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
.idea/**/aws.xml
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
.idea/**/contentModel.xml
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
.idea/**/dataSources/
|
|
144
|
+
.idea/**/dataSources.ids
|
|
145
|
+
.idea/**/dataSources.local.xml
|
|
146
|
+
.idea/**/sqlDataSources.xml
|
|
147
|
+
.idea/**/dynamic.xml
|
|
148
|
+
.idea/**/uiDesigner.xml
|
|
149
|
+
.idea/**/dbnavigator.xml
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
.idea/**/gradle.xml
|
|
153
|
+
.idea/**/libraries
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
cmake-build-*/
|
|
157
|
+
.stignore
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
_temp
|
|
161
|
+
dist
|
|
162
|
+
tests
|
panda_cli-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: panda-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Declarative CLI framework: commands are Pydantic models, built on Click
|
|
5
|
+
Author-email: meleshuk0804@gmail.com
|
|
6
|
+
Keywords: cli,click,command-line,declarative,pydantic
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: click>=8.3.1
|
|
17
|
+
Requires-Dist: pydantic>=2.12.5
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# panda-cli
|
|
21
|
+
|
|
22
|
+
A lightweight declarative CLI framework built on top of Click and Pydantic.
|
|
23
|
+
|
|
24
|
+
Define commands as Python classes, describe arguments with type hints,
|
|
25
|
+
and build CLI trees using simple class composition.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Declarative CLI commands
|
|
32
|
+
- Type-safe arguments
|
|
33
|
+
- Automatic CLI tree generation
|
|
34
|
+
- Built-in validation via Pydantic
|
|
35
|
+
- Validation errors reported in command-line terms (`--port`, not `port`)
|
|
36
|
+
- Minimal boilerplate
|
|
37
|
+
- Nested command groups
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install panda-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Quick Example
|
|
49
|
+
```python
|
|
50
|
+
from panda_cli import BaseCommand, Option, BaseGroup
|
|
51
|
+
from panda_cli import Config
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class WebCli(BaseCommand):
|
|
55
|
+
""" Web server """ # automaticly add to web --help
|
|
56
|
+
__cli_name__ = "web"
|
|
57
|
+
|
|
58
|
+
config=Config(fields_name_to_flag=True) # if True auto gen flag by field name host -> --host
|
|
59
|
+
|
|
60
|
+
host: str = Option(
|
|
61
|
+
"--host",
|
|
62
|
+
default="127.0.0.1",
|
|
63
|
+
description="Host to listen on",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
port: int = Option(
|
|
67
|
+
"--port",
|
|
68
|
+
default=8000,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
methods: list[str] # --methods GET --methods POST -> ['GET', 'POST']
|
|
72
|
+
|
|
73
|
+
map: dict[str, str] # --map key=value --map key2=value2 -> {'key': 'value', 'key2': 'value2'}
|
|
74
|
+
|
|
75
|
+
def __exec__(self) -> None:
|
|
76
|
+
""" Automaticly runs when command is called """
|
|
77
|
+
|
|
78
|
+
print(f"Host: {self.host}")
|
|
79
|
+
print(f"Port: {self.port}")
|
|
80
|
+
print(f"Methods: {self.methods}")
|
|
81
|
+
print(f"Map: {self.map}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MainCli(BaseGroup):
|
|
85
|
+
__cli_name__ = "main"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
web: WebCli
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
MainCli.run()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## CLI
|
|
101
|
+
```bash
|
|
102
|
+
uv run examples/web.py --help
|
|
103
|
+
|
|
104
|
+
Usage: main [OPTIONS] COMMAND [ARGS]...
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
--help Show this message and exit.
|
|
108
|
+
|
|
109
|
+
Commands:
|
|
110
|
+
web Web server
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
uv run examples/web.py web --help
|
|
115
|
+
|
|
116
|
+
Usage: main web [OPTIONS]
|
|
117
|
+
|
|
118
|
+
Web server
|
|
119
|
+
|
|
120
|
+
Options:
|
|
121
|
+
--host TEXT Host to listen on [default: 127.0.0.1]
|
|
122
|
+
--port INTEGER [default: 8000]
|
|
123
|
+
--methods Iterable TEXT... [required]
|
|
124
|
+
--map TEXT=TEXT [required]
|
|
125
|
+
--help Show this message and exit.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## Validation
|
|
130
|
+
|
|
131
|
+
Any Pydantic validator works, and failures are shown the way a CLI user
|
|
132
|
+
expects them — not as a traceback:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from pydantic import Field
|
|
136
|
+
|
|
137
|
+
class WebCli(BaseCommand):
|
|
138
|
+
__cli_name__ = "web"
|
|
139
|
+
port: int = Field(default=8000, ge=1, le=65535)
|
|
140
|
+
|
|
141
|
+
def __exec__(self) -> None: ...
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
$ web --port 99999
|
|
146
|
+
|
|
147
|
+
Usage: web [OPTIONS]
|
|
148
|
+
Try 'web --help' for help.
|
|
149
|
+
|
|
150
|
+
Error: Invalid value for '--port': Input should be less than or equal to 65535
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Environment variables
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
host: str = Option("--host", default="127.0.0.1", envvar="APP_HOST")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
## Embedding into an existing Click app
|
|
162
|
+
|
|
163
|
+
`run()` is the entry point for `console_scripts`, but the built command is
|
|
164
|
+
also available on its own — useful for mounting a panda-cli tree inside a
|
|
165
|
+
Click application, and for testing:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import click
|
|
169
|
+
|
|
170
|
+
cli = click.Group("myapp")
|
|
171
|
+
cli.add_command(MainCli.as_click())
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`MainCli.spec()` returns the same tree as backend-independent dataclasses,
|
|
175
|
+
without touching Click at all.
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
## Architecture
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
declaration BaseCommand / BaseGroup — what the user writes
|
|
182
|
+
introspect model -> Spec — the only module that imports pydantic
|
|
183
|
+
backend Spec -> click.Command — the only module that imports click
|
|
184
|
+
runtime argv -> validate -> __exec__
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Subcommands are declared as fields (`web: WebCli`) but are not model fields:
|
|
188
|
+
the metaclass moves them into the command tree before Pydantic sees them, so
|
|
189
|
+
a group stays an ordinary model with its own options.
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
make test # run the test suite
|
|
196
|
+
make cov # coverage
|
|
197
|
+
make regression # tests guarding previously fixed bugs
|
|
198
|
+
make check # lint + tests
|
|
199
|
+
```
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# panda-cli
|
|
2
|
+
|
|
3
|
+
A lightweight declarative CLI framework built on top of Click and Pydantic.
|
|
4
|
+
|
|
5
|
+
Define commands as Python classes, describe arguments with type hints,
|
|
6
|
+
and build CLI trees using simple class composition.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Declarative CLI commands
|
|
13
|
+
- Type-safe arguments
|
|
14
|
+
- Automatic CLI tree generation
|
|
15
|
+
- Built-in validation via Pydantic
|
|
16
|
+
- Validation errors reported in command-line terms (`--port`, not `port`)
|
|
17
|
+
- Minimal boilerplate
|
|
18
|
+
- Nested command groups
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install panda-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Quick Example
|
|
30
|
+
```python
|
|
31
|
+
from panda_cli import BaseCommand, Option, BaseGroup
|
|
32
|
+
from panda_cli import Config
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WebCli(BaseCommand):
|
|
36
|
+
""" Web server """ # automaticly add to web --help
|
|
37
|
+
__cli_name__ = "web"
|
|
38
|
+
|
|
39
|
+
config=Config(fields_name_to_flag=True) # if True auto gen flag by field name host -> --host
|
|
40
|
+
|
|
41
|
+
host: str = Option(
|
|
42
|
+
"--host",
|
|
43
|
+
default="127.0.0.1",
|
|
44
|
+
description="Host to listen on",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
port: int = Option(
|
|
48
|
+
"--port",
|
|
49
|
+
default=8000,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
methods: list[str] # --methods GET --methods POST -> ['GET', 'POST']
|
|
53
|
+
|
|
54
|
+
map: dict[str, str] # --map key=value --map key2=value2 -> {'key': 'value', 'key2': 'value2'}
|
|
55
|
+
|
|
56
|
+
def __exec__(self) -> None:
|
|
57
|
+
""" Automaticly runs when command is called """
|
|
58
|
+
|
|
59
|
+
print(f"Host: {self.host}")
|
|
60
|
+
print(f"Port: {self.port}")
|
|
61
|
+
print(f"Methods: {self.methods}")
|
|
62
|
+
print(f"Map: {self.map}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MainCli(BaseGroup):
|
|
66
|
+
__cli_name__ = "main"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
web: WebCli
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main():
|
|
73
|
+
MainCli.run()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## CLI
|
|
82
|
+
```bash
|
|
83
|
+
uv run examples/web.py --help
|
|
84
|
+
|
|
85
|
+
Usage: main [OPTIONS] COMMAND [ARGS]...
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
--help Show this message and exit.
|
|
89
|
+
|
|
90
|
+
Commands:
|
|
91
|
+
web Web server
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
uv run examples/web.py web --help
|
|
96
|
+
|
|
97
|
+
Usage: main web [OPTIONS]
|
|
98
|
+
|
|
99
|
+
Web server
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
--host TEXT Host to listen on [default: 127.0.0.1]
|
|
103
|
+
--port INTEGER [default: 8000]
|
|
104
|
+
--methods Iterable TEXT... [required]
|
|
105
|
+
--map TEXT=TEXT [required]
|
|
106
|
+
--help Show this message and exit.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Validation
|
|
111
|
+
|
|
112
|
+
Any Pydantic validator works, and failures are shown the way a CLI user
|
|
113
|
+
expects them — not as a traceback:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from pydantic import Field
|
|
117
|
+
|
|
118
|
+
class WebCli(BaseCommand):
|
|
119
|
+
__cli_name__ = "web"
|
|
120
|
+
port: int = Field(default=8000, ge=1, le=65535)
|
|
121
|
+
|
|
122
|
+
def __exec__(self) -> None: ...
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
$ web --port 99999
|
|
127
|
+
|
|
128
|
+
Usage: web [OPTIONS]
|
|
129
|
+
Try 'web --help' for help.
|
|
130
|
+
|
|
131
|
+
Error: Invalid value for '--port': Input should be less than or equal to 65535
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
## Environment variables
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
host: str = Option("--host", default="127.0.0.1", envvar="APP_HOST")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## Embedding into an existing Click app
|
|
143
|
+
|
|
144
|
+
`run()` is the entry point for `console_scripts`, but the built command is
|
|
145
|
+
also available on its own — useful for mounting a panda-cli tree inside a
|
|
146
|
+
Click application, and for testing:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import click
|
|
150
|
+
|
|
151
|
+
cli = click.Group("myapp")
|
|
152
|
+
cli.add_command(MainCli.as_click())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`MainCli.spec()` returns the same tree as backend-independent dataclasses,
|
|
156
|
+
without touching Click at all.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Architecture
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
declaration BaseCommand / BaseGroup — what the user writes
|
|
163
|
+
introspect model -> Spec — the only module that imports pydantic
|
|
164
|
+
backend Spec -> click.Command — the only module that imports click
|
|
165
|
+
runtime argv -> validate -> __exec__
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Subcommands are declared as fields (`web: WebCli`) but are not model fields:
|
|
169
|
+
the metaclass moves them into the command tree before Pydantic sees them, so
|
|
170
|
+
a group stays an ordinary model with its own options.
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
make test # run the test suite
|
|
177
|
+
make cov # coverage
|
|
178
|
+
make regression # tests guarding previously fixed bugs
|
|
179
|
+
make check # lint + tests
|
|
180
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from panda_cli import BaseCommand, Option, BaseGroup
|
|
2
|
+
from panda_cli import Config
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WebCli(BaseCommand):
|
|
6
|
+
"""Web server""" # automaticly add to web --help
|
|
7
|
+
|
|
8
|
+
__cli_name__ = "web"
|
|
9
|
+
|
|
10
|
+
config = Config(fields_name_to_flag=True) # if True auto gen flag by field name host -> --host
|
|
11
|
+
|
|
12
|
+
host: str = Option(
|
|
13
|
+
"--host",
|
|
14
|
+
default="127.0.0.1",
|
|
15
|
+
description="Host to listen on",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
port: int = Option(
|
|
19
|
+
"--port",
|
|
20
|
+
default=8000,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
methods: list[str] # --methods GET --methods POST -> ['GET', 'POST']
|
|
24
|
+
|
|
25
|
+
map: dict[str, str] # --map key=value --map key2=value2 -> {'key': 'value', 'key2': 'value2'}
|
|
26
|
+
|
|
27
|
+
def __exec__(self) -> None:
|
|
28
|
+
"""Automaticly runs when command is called"""
|
|
29
|
+
|
|
30
|
+
print(f"Host: {self.host}")
|
|
31
|
+
print(f"Port: {self.port}")
|
|
32
|
+
print(f"Methods: {self.methods}")
|
|
33
|
+
print(f"Map: {self.map}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MainCli(BaseGroup):
|
|
37
|
+
__cli_name__ = "main"
|
|
38
|
+
|
|
39
|
+
web: WebCli
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
MainCli.run()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
panda_cli-0.3.0/makefile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.PHONY: major minor patch test cov regression lint check
|
|
2
|
+
|
|
3
|
+
PYPROJECT := pyproject.toml
|
|
4
|
+
INIT_FILE := src/panda_cli/__init__.py
|
|
5
|
+
|
|
6
|
+
# Читаем текущую версию из pyproject.toml
|
|
7
|
+
CURRENT := $(shell grep '^version = ' $(PYPROJECT) | sed 's/version = "\(.*\)"/\1/')
|
|
8
|
+
MAJOR := $(shell echo $(CURRENT) | cut -d. -f1)
|
|
9
|
+
MINOR := $(shell echo $(CURRENT) | cut -d. -f2)
|
|
10
|
+
PATCH := $(shell echo $(CURRENT) | cut -d. -f3)
|
|
11
|
+
|
|
12
|
+
define bump
|
|
13
|
+
@echo UPDATED "$(CURRENT) → $(1)"
|
|
14
|
+
@sed -i 's/^version = ".*"/version = "$(1)"/' $(PYPROJECT)
|
|
15
|
+
@sed -i 's/^__version__ = ".*"/__version__ = "$(1)"/' $(INIT_FILE)
|
|
16
|
+
endef
|
|
17
|
+
|
|
18
|
+
major:
|
|
19
|
+
$(call bump,$(shell echo $$(($(MAJOR)+1))).0.0)
|
|
20
|
+
|
|
21
|
+
minor:
|
|
22
|
+
$(call bump,$(MAJOR).$(shell echo $$(($(MINOR)+1))).0)
|
|
23
|
+
|
|
24
|
+
patch:
|
|
25
|
+
$(call bump,$(MAJOR).$(MINOR).$(shell echo $$(($(PATCH)+1))))
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
uv run pytest
|
|
29
|
+
|
|
30
|
+
cov:
|
|
31
|
+
uv run pytest --cov
|
|
32
|
+
|
|
33
|
+
regression: # тесты на закрытые баги
|
|
34
|
+
uv run pytest -m regression -rX
|
|
35
|
+
|
|
36
|
+
lint:
|
|
37
|
+
uv run ruff check .
|
|
38
|
+
uv run ruff format --check .
|
|
39
|
+
|
|
40
|
+
check: lint test
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "panda-cli"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Declarative CLI framework: commands are Pydantic models, built on Click"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ email = "meleshuk0804@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
keywords = ["cli", "click", "pydantic", "command-line", "declarative"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"click>=8.3.1",
|
|
23
|
+
"pydantic>=2.12.5",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/panda_cli"]
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"mypy>=1.19.1",
|
|
37
|
+
"pytest>=9.1.1",
|
|
38
|
+
"pytest-cov>=7.1.0",
|
|
39
|
+
"ruff>=0.15.4",
|
|
40
|
+
"twine>=6.2.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
addopts = "-q --strict-markers --strict-config"
|
|
46
|
+
markers = [
|
|
47
|
+
"regression: тест на конкретный закрытый баг — не удалять при рефакторинге",
|
|
48
|
+
]
|
|
49
|
+
xfail_strict = true
|
|
50
|
+
|
|
51
|
+
[tool.coverage.run]
|
|
52
|
+
source = ["panda_cli"]
|
|
53
|
+
branch = true
|
|
54
|
+
|
|
55
|
+
[tool.coverage.report]
|
|
56
|
+
show_missing = true
|
|
57
|
+
exclude_lines = ["pragma: no cover", "raise NotImplementedError", "if t.TYPE_CHECKING:"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
src = [".", "src"]
|
|
62
|
+
exclude = ["_temp", "dist", ".venv"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
66
|
+
ignore = [
|
|
67
|
+
"RUF003", # кириллица в комментариях
|
|
68
|
+
"RUF001", # кириллица в строках — здесь это нормально
|
|
69
|
+
"RUF002", # кириллица в docstring'ах
|
|
70
|
+
"RUF012", # ClassVar для mutable-атрибутов: ложное срабатывание на полях pydantic
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint.isort]
|
|
74
|
+
known-first-party = ["panda_cli", "tests"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint.per-file-ignores]
|
|
77
|
+
# пример повторяет README дословно — форматирование импортов оставлено как в доке
|
|
78
|
+
"examples/*.py" = ["I001"]
|
|
79
|
+
|
|
80
|
+
[tool.mypy]
|
|
81
|
+
python_version = "3.12"
|
|
82
|
+
strict = true
|
|
83
|
+
files = ["src/panda_cli", "tests"]
|