sigil-cli 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.
- sigil_cli-0.1.0/LICENSE +19 -0
- sigil_cli-0.1.0/PKG-INFO +212 -0
- sigil_cli-0.1.0/README.md +188 -0
- sigil_cli-0.1.0/pyproject.toml +32 -0
- sigil_cli-0.1.0/setup.cfg +4 -0
- sigil_cli-0.1.0/src/sigil/__init__.py +25 -0
- sigil_cli-0.1.0/src/sigil/entrypoint.py +25 -0
- sigil_cli-0.1.0/src/sigil/models/__init__.py +12 -0
- sigil_cli-0.1.0/src/sigil/models/argparser.py +11 -0
- sigil_cli-0.1.0/src/sigil/models/argument.py +22 -0
- sigil_cli-0.1.0/src/sigil/models/command.py +28 -0
- sigil_cli-0.1.0/src/sigil/models/script.py +8 -0
- sigil_cli-0.1.0/src/sigil/stages/__init__.py +11 -0
- sigil_cli-0.1.0/src/sigil/stages/builder.py +78 -0
- sigil_cli-0.1.0/src/sigil/stages/resolver.py +48 -0
- sigil_cli-0.1.0/src/sigil/stages/script_loader.py +55 -0
- sigil_cli-0.1.0/src/sigil/stages/yaml_reader.py +86 -0
- sigil_cli-0.1.0/src/sigil_cli.egg-info/PKG-INFO +212 -0
- sigil_cli-0.1.0/src/sigil_cli.egg-info/SOURCES.txt +25 -0
- sigil_cli-0.1.0/src/sigil_cli.egg-info/dependency_links.txt +1 -0
- sigil_cli-0.1.0/src/sigil_cli.egg-info/requires.txt +9 -0
- sigil_cli-0.1.0/src/sigil_cli.egg-info/top_level.txt +1 -0
- sigil_cli-0.1.0/tests/test_builder.py +53 -0
- sigil_cli-0.1.0/tests/test_integration.py +78 -0
- sigil_cli-0.1.0/tests/test_reader.py +85 -0
- sigil_cli-0.1.0/tests/test_resolver.py +27 -0
- sigil_cli-0.1.0/tests/test_scriptloader.py +59 -0
sigil_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Kenzo Staelens
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
sigil_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sigil-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Configuration driven CLI builder with subcommands and script loading
|
|
5
|
+
Author-email: Kenzo Staelens <kenzostael@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/sigil
|
|
8
|
+
Project-URL: Repository, https://github.com/yourusername/sigil
|
|
9
|
+
Project-URL: Issues, https://github.com/yourusername/sigil/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Provides-Extra: completion
|
|
18
|
+
Requires-Dist: argcomplete>=3.0; extra == "completion"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Sigil
|
|
26
|
+
|
|
27
|
+
> Declarative argparse, without the CLI boilerplate.
|
|
28
|
+
|
|
29
|
+
Sigil is a lightweight, declarative CLI framework for Python. Define your command tree in YAML (or any other format), and sigil builds the `argparse` parser on the fly. Complete with subcommands and dynamic script loading.
|
|
30
|
+
It plays nicely with `argcomplete` out of the box.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- Declarative command hierarchies (parents, subparsers, defaults)
|
|
37
|
+
- Each command can point to a dynamically imported Python script
|
|
38
|
+
- `argcomplete` integration for tab‑completion
|
|
39
|
+
- Pluggable data sources – YAML is the default, but JSON, TOML, or a dict are trivial to swap in
|
|
40
|
+
- No boilerplate argparse code in your main logic
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install sigil-cli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
or include argcomplete
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install sigil-cli[completion]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 0. Recommended file structure
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
project_root/
|
|
60
|
+
├── mycli.py # drop‑in entry script (alias this)
|
|
61
|
+
├── manifest.yml # lists all YAML config files to load
|
|
62
|
+
├── yml/
|
|
63
|
+
│ ├── root.yml # root command definition
|
|
64
|
+
│ ├── root_run.yml # subcommand definition(s)
|
|
65
|
+
│ └── ...
|
|
66
|
+
└── scripts/
|
|
67
|
+
├── run.py # implements the 'run' command
|
|
68
|
+
└── ... # other scripts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 1. Entry script
|
|
72
|
+
|
|
73
|
+
Create `mycli.py`:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
#!/usr/bin/env python3
|
|
77
|
+
from pathlib import Path
|
|
78
|
+
from sigil import run_from_config
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
run_from_config(Path(__file__).parent)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Configuration files
|
|
85
|
+
|
|
86
|
+
List all your YAML definitions in `manifest.yml`:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
---
|
|
90
|
+
- root.yml
|
|
91
|
+
- root_run.yml
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Define the root command in `root.yml`:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
root:
|
|
98
|
+
name: mycli
|
|
99
|
+
script_dir: scripts
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Define a subcommand in `root_run.yml`:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
root_run:
|
|
106
|
+
name: run
|
|
107
|
+
parent: root
|
|
108
|
+
help: command utility to run containers
|
|
109
|
+
script: run
|
|
110
|
+
args:
|
|
111
|
+
- help: port to run, autoincrements from 8080
|
|
112
|
+
name:
|
|
113
|
+
- -p
|
|
114
|
+
- --port
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. Write the script
|
|
118
|
+
|
|
119
|
+
Create `scripts/run.py`:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import argparse
|
|
123
|
+
from typing import Any
|
|
124
|
+
|
|
125
|
+
def run(args: argparse.Namespace, ctx: dict[str, Any]) -> None:
|
|
126
|
+
port = getattr(args, "port", 8080)
|
|
127
|
+
port = find_next_free_port_logic(port)
|
|
128
|
+
print(f"Running container on port {port}")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 4. Run it
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
chmod +x mycli.py
|
|
135
|
+
./mycli.py run --port 9000
|
|
136
|
+
# Running container on port 9000
|
|
137
|
+
|
|
138
|
+
./mycli.py run
|
|
139
|
+
# Running container on port 8080
|
|
140
|
+
|
|
141
|
+
./mycli.py run
|
|
142
|
+
# Running container on port 8081
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Configuration Reference
|
|
146
|
+
|
|
147
|
+
### Root
|
|
148
|
+
|
|
149
|
+
| Field | Description |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `name` | Program name (used as `prog` in argparse) |
|
|
152
|
+
| `script_dir` | Directory (relative to the config root) where command scripts are located |
|
|
153
|
+
|
|
154
|
+
### Command
|
|
155
|
+
|
|
156
|
+
| Field | Description |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `name` | Subcommand name |
|
|
159
|
+
| `parent` | Parent command (must exist elsewhere in a config) |
|
|
160
|
+
| `help` | Help text for this subcommand |
|
|
161
|
+
| `script` | Python module name (without `.py`) inside `script_dir` or as absolute path |
|
|
162
|
+
| `args` | List of argument definitions (see below) |
|
|
163
|
+
| `default` | If `true`, this subcommand is used when no subcommand is given |
|
|
164
|
+
|
|
165
|
+
### Argument
|
|
166
|
+
|
|
167
|
+
Each argument entry can be a plain dict which maps 1-to-1 with argparse `add_argument`, except name which maps it's `*args`
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
- name: ["-p", "--port"] # or a single string, e.g. "positional"
|
|
171
|
+
required: false
|
|
172
|
+
default: 8069
|
|
173
|
+
help: "port number"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The `name` field can be `--flag` for flags or a string for positional arguments.
|
|
177
|
+
Both literal string and list of strings are supported.
|
|
178
|
+
|
|
179
|
+
## Tab‑Completion (argcomplete)
|
|
180
|
+
|
|
181
|
+
Argus registers itself with `argcomplete` automatically if available on your system.
|
|
182
|
+
To enable completion, install [argcomplete](https://github.com/kislyuk/argcomplete) and activate it for your entry script:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pip install argcomplete
|
|
186
|
+
activate-global-python-argcomplete
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Then run your script and hit <kbd>Tab</kbd> – subcommands and flags will complete.
|
|
190
|
+
|
|
191
|
+
## Pluggable Backends
|
|
192
|
+
|
|
193
|
+
Sigil uses yaml by default, but you can supply any loader that returns a `dict[str, ParserConfig]`:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from sigil import run_from_config
|
|
197
|
+
|
|
198
|
+
# Use JSON instead:
|
|
199
|
+
class JsonReader:
|
|
200
|
+
@classmethod
|
|
201
|
+
def load(cls, config_root):
|
|
202
|
+
# read *.json, parse, convert to dict of ParserConfig
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
run_from_config("/path/to/config", loader_class=JsonReader)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
You can also pass a pre‑loaded dictionary directly by wrapping it:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
run_from_config(my_dict, loader_class=DictLoader)
|
|
212
|
+
```
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Sigil
|
|
2
|
+
|
|
3
|
+
> Declarative argparse, without the CLI boilerplate.
|
|
4
|
+
|
|
5
|
+
Sigil is a lightweight, declarative CLI framework for Python. Define your command tree in YAML (or any other format), and sigil builds the `argparse` parser on the fly. Complete with subcommands and dynamic script loading.
|
|
6
|
+
It plays nicely with `argcomplete` out of the box.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Declarative command hierarchies (parents, subparsers, defaults)
|
|
13
|
+
- Each command can point to a dynamically imported Python script
|
|
14
|
+
- `argcomplete` integration for tab‑completion
|
|
15
|
+
- Pluggable data sources – YAML is the default, but JSON, TOML, or a dict are trivial to swap in
|
|
16
|
+
- No boilerplate argparse code in your main logic
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install sigil-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or include argcomplete
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install sigil-cli[completion]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 0. Recommended file structure
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
project_root/
|
|
36
|
+
├── mycli.py # drop‑in entry script (alias this)
|
|
37
|
+
├── manifest.yml # lists all YAML config files to load
|
|
38
|
+
├── yml/
|
|
39
|
+
│ ├── root.yml # root command definition
|
|
40
|
+
│ ├── root_run.yml # subcommand definition(s)
|
|
41
|
+
│ └── ...
|
|
42
|
+
└── scripts/
|
|
43
|
+
├── run.py # implements the 'run' command
|
|
44
|
+
└── ... # other scripts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 1. Entry script
|
|
48
|
+
|
|
49
|
+
Create `mycli.py`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
#!/usr/bin/env python3
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from sigil import run_from_config
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
run_from_config(Path(__file__).parent)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Configuration files
|
|
61
|
+
|
|
62
|
+
List all your YAML definitions in `manifest.yml`:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
---
|
|
66
|
+
- root.yml
|
|
67
|
+
- root_run.yml
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Define the root command in `root.yml`:
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
root:
|
|
74
|
+
name: mycli
|
|
75
|
+
script_dir: scripts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Define a subcommand in `root_run.yml`:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
root_run:
|
|
82
|
+
name: run
|
|
83
|
+
parent: root
|
|
84
|
+
help: command utility to run containers
|
|
85
|
+
script: run
|
|
86
|
+
args:
|
|
87
|
+
- help: port to run, autoincrements from 8080
|
|
88
|
+
name:
|
|
89
|
+
- -p
|
|
90
|
+
- --port
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Write the script
|
|
94
|
+
|
|
95
|
+
Create `scripts/run.py`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import argparse
|
|
99
|
+
from typing import Any
|
|
100
|
+
|
|
101
|
+
def run(args: argparse.Namespace, ctx: dict[str, Any]) -> None:
|
|
102
|
+
port = getattr(args, "port", 8080)
|
|
103
|
+
port = find_next_free_port_logic(port)
|
|
104
|
+
print(f"Running container on port {port}")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 4. Run it
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
chmod +x mycli.py
|
|
111
|
+
./mycli.py run --port 9000
|
|
112
|
+
# Running container on port 9000
|
|
113
|
+
|
|
114
|
+
./mycli.py run
|
|
115
|
+
# Running container on port 8080
|
|
116
|
+
|
|
117
|
+
./mycli.py run
|
|
118
|
+
# Running container on port 8081
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Configuration Reference
|
|
122
|
+
|
|
123
|
+
### Root
|
|
124
|
+
|
|
125
|
+
| Field | Description |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `name` | Program name (used as `prog` in argparse) |
|
|
128
|
+
| `script_dir` | Directory (relative to the config root) where command scripts are located |
|
|
129
|
+
|
|
130
|
+
### Command
|
|
131
|
+
|
|
132
|
+
| Field | Description |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `name` | Subcommand name |
|
|
135
|
+
| `parent` | Parent command (must exist elsewhere in a config) |
|
|
136
|
+
| `help` | Help text for this subcommand |
|
|
137
|
+
| `script` | Python module name (without `.py`) inside `script_dir` or as absolute path |
|
|
138
|
+
| `args` | List of argument definitions (see below) |
|
|
139
|
+
| `default` | If `true`, this subcommand is used when no subcommand is given |
|
|
140
|
+
|
|
141
|
+
### Argument
|
|
142
|
+
|
|
143
|
+
Each argument entry can be a plain dict which maps 1-to-1 with argparse `add_argument`, except name which maps it's `*args`
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
- name: ["-p", "--port"] # or a single string, e.g. "positional"
|
|
147
|
+
required: false
|
|
148
|
+
default: 8069
|
|
149
|
+
help: "port number"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The `name` field can be `--flag` for flags or a string for positional arguments.
|
|
153
|
+
Both literal string and list of strings are supported.
|
|
154
|
+
|
|
155
|
+
## Tab‑Completion (argcomplete)
|
|
156
|
+
|
|
157
|
+
Argus registers itself with `argcomplete` automatically if available on your system.
|
|
158
|
+
To enable completion, install [argcomplete](https://github.com/kislyuk/argcomplete) and activate it for your entry script:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pip install argcomplete
|
|
162
|
+
activate-global-python-argcomplete
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Then run your script and hit <kbd>Tab</kbd> – subcommands and flags will complete.
|
|
166
|
+
|
|
167
|
+
## Pluggable Backends
|
|
168
|
+
|
|
169
|
+
Sigil uses yaml by default, but you can supply any loader that returns a `dict[str, ParserConfig]`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from sigil import run_from_config
|
|
173
|
+
|
|
174
|
+
# Use JSON instead:
|
|
175
|
+
class JsonReader:
|
|
176
|
+
@classmethod
|
|
177
|
+
def load(cls, config_root):
|
|
178
|
+
# read *.json, parse, convert to dict of ParserConfig
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
run_from_config("/path/to/config", loader_class=JsonReader)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
You can also pass a pre‑loaded dictionary directly by wrapping it:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
run_from_config(my_dict, loader_class=DictLoader)
|
|
188
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sigil-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Configuration driven CLI builder with subcommands and script loading"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "Kenzo Staelens", email = "kenzostael@gmail.com"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"pyyaml>=6.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
completion = ["argcomplete>=3.0"]
|
|
24
|
+
dev = ["pytest", "pytest-cov", "ruff"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/yourusername/sigil"
|
|
28
|
+
Repository = "https://github.com/yourusername/sigil"
|
|
29
|
+
Issues = "https://github.com/yourusername/sigil/issues"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .entrypoint import run_from_config
|
|
2
|
+
from .models import (
|
|
3
|
+
Argument,
|
|
4
|
+
LibArgParser,
|
|
5
|
+
ParserConfig,
|
|
6
|
+
SubcommandModule,
|
|
7
|
+
)
|
|
8
|
+
from .stages import (
|
|
9
|
+
Builder,
|
|
10
|
+
Resolver,
|
|
11
|
+
ScriptLoader,
|
|
12
|
+
YamlReader,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
run_from_config,
|
|
17
|
+
Argument,
|
|
18
|
+
LibArgParser,
|
|
19
|
+
ParserConfig,
|
|
20
|
+
SubcommandModule,
|
|
21
|
+
YamlReader,
|
|
22
|
+
Resolver,
|
|
23
|
+
Builder,
|
|
24
|
+
ScriptLoader,
|
|
25
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .stages import Builder, Resolver, ScriptLoader, YamlReader
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# default to yamlloader, use whatever datastore you feel like
|
|
7
|
+
def run_from_config(config_root: str, loader_class = YamlReader):
|
|
8
|
+
raw_data = loader_class.load(config_root)
|
|
9
|
+
resolved = Resolver.resolve_inheritance(raw_data)
|
|
10
|
+
parser = Builder.build(resolved)
|
|
11
|
+
args, other_args = parser.parse_known_args()
|
|
12
|
+
scripts = ScriptLoader.get_scripts(args, 'root', parser, {'root': resolved})
|
|
13
|
+
|
|
14
|
+
execution_context: dict[str, Any] = {'other_args': other_args}
|
|
15
|
+
for script in scripts:
|
|
16
|
+
try:
|
|
17
|
+
module = ScriptLoader.import_module(
|
|
18
|
+
config_root,
|
|
19
|
+
resolved.script_dir,
|
|
20
|
+
script
|
|
21
|
+
)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print(f'failed to load script "{script}"\n\t{e}')
|
|
24
|
+
return
|
|
25
|
+
module.run(args, execution_context)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
# because setting default subommands is kind of a pain in std. argparse
|
|
4
|
+
|
|
5
|
+
class LibArgParser(argparse.ArgumentParser):
|
|
6
|
+
def set_default_subparser(self, name, args=None, positional_args=0):
|
|
7
|
+
for action in self._actions:
|
|
8
|
+
if not isinstance(action, argparse._SubParsersAction):
|
|
9
|
+
continue
|
|
10
|
+
if name in action._name_parser_map.keys():
|
|
11
|
+
action.default=name
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Argument:
|
|
7
|
+
name: list[str]
|
|
8
|
+
required: bool = False
|
|
9
|
+
default: bool = False
|
|
10
|
+
help: str | None = None
|
|
11
|
+
action: str | None = None
|
|
12
|
+
# anything we haven't explicitly defined.. yet
|
|
13
|
+
kw: dict[str, Any] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
# Source - https://stackoverflow.com/a/74459763
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def factory(cls, **kwargs: dict):
|
|
19
|
+
name = kwargs.get('name')
|
|
20
|
+
if isinstance(name, str):
|
|
21
|
+
kwargs['name'] = [name]
|
|
22
|
+
return cls(**{k: kwargs[k] for k in kwargs if k in cls.__match_args__})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from .argument import Argument
|
|
5
|
+
|
|
6
|
+
_logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ParserConfig:
|
|
10
|
+
name: str
|
|
11
|
+
script_dir: str | None = None # only for root
|
|
12
|
+
help: str | None = None
|
|
13
|
+
args: list[Argument] = field(default_factory=list)
|
|
14
|
+
script: str | None = None
|
|
15
|
+
parent: str | None = None
|
|
16
|
+
default: bool = False
|
|
17
|
+
load: bool = True
|
|
18
|
+
subparsers: 'dict[str, ParserConfig]' = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def factory(cls, **kwargs):
|
|
22
|
+
for k in kwargs:
|
|
23
|
+
if k not in cls.__match_args__:
|
|
24
|
+
_logger.warning(
|
|
25
|
+
f'found unknown key "{k}" in parser '
|
|
26
|
+
f'"{kwargs.get("name")}", ignoring'
|
|
27
|
+
)
|
|
28
|
+
return cls(**{k: kwargs[k] for k in kwargs if k in cls.__match_args__})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from sigil.models import Argument, LibArgParser, ParserConfig
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
import argcomplete
|
|
5
|
+
USE_ARGCOMPLETE = True
|
|
6
|
+
except Exception:
|
|
7
|
+
USE_ARGCOMPLETE=False
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
class Builder:
|
|
14
|
+
# also in place modification, returning just causes more problems anyway
|
|
15
|
+
@classmethod
|
|
16
|
+
def _build_subparsers(
|
|
17
|
+
cls,
|
|
18
|
+
base_parser: LibArgParser,
|
|
19
|
+
parser_data: ParserConfig
|
|
20
|
+
) -> None:
|
|
21
|
+
if parser_data.subparsers:
|
|
22
|
+
argparse_group = base_parser.add_subparsers(
|
|
23
|
+
dest=parser_data.name,
|
|
24
|
+
title='subcommands',
|
|
25
|
+
)
|
|
26
|
+
last_default = None # find the last default
|
|
27
|
+
for _, subcommand in parser_data.subparsers.items():
|
|
28
|
+
subcommand_name = subcommand.name
|
|
29
|
+
subcmd_help = subcommand.help
|
|
30
|
+
argparser = argparse_group.add_parser(subcommand_name, help=subcmd_help)
|
|
31
|
+
for arg in subcommand.args:
|
|
32
|
+
cls.attach_argument(argparser, arg)
|
|
33
|
+
if subcommand.default:
|
|
34
|
+
if last_default:
|
|
35
|
+
_logger.warning(
|
|
36
|
+
f'last default {last_default} overridden '
|
|
37
|
+
f'with {subcommand.name}'
|
|
38
|
+
)
|
|
39
|
+
last_default = subcommand.name
|
|
40
|
+
if subcommand.subparsers:
|
|
41
|
+
cls._build_subparsers(argparser, subcommand)
|
|
42
|
+
if last_default:
|
|
43
|
+
base_parser.set_default_subparser(last_default)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def attach_argument(
|
|
47
|
+
cls,
|
|
48
|
+
parser: LibArgParser,
|
|
49
|
+
argument_config: Argument
|
|
50
|
+
) -> None:
|
|
51
|
+
conditional_args: dict[str, Any] = {}
|
|
52
|
+
if any(x.startswith('-') for x in argument_config.name):
|
|
53
|
+
# if no '--name' or '-n' exists it's required by default
|
|
54
|
+
conditional_args['required']=argument_config.required
|
|
55
|
+
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
*argument_config.name,
|
|
58
|
+
help=argument_config.help,
|
|
59
|
+
action=argument_config.action,
|
|
60
|
+
default=argument_config.default,
|
|
61
|
+
**(argument_config.kw | conditional_args)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def build(
|
|
66
|
+
cls,
|
|
67
|
+
root_data: ParserConfig
|
|
68
|
+
) -> LibArgParser:
|
|
69
|
+
root_name = root_data.name
|
|
70
|
+
root_parser = LibArgParser(prog=root_name)
|
|
71
|
+
for arg in root_data.args or []:
|
|
72
|
+
cls.attach_argument(root_parser, arg)
|
|
73
|
+
|
|
74
|
+
cls._build_subparsers(root_parser, root_data)
|
|
75
|
+
if USE_ARGCOMPLETE:
|
|
76
|
+
argcomplete.autocomplete(root_parser)
|
|
77
|
+
|
|
78
|
+
return root_parser
|