python-base-command 0.1.2__tar.gz → 0.1.4__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.
- {python_base_command-0.1.2 → python_base_command-0.1.4}/PKG-INFO +57 -27
- {python_base_command-0.1.2 → python_base_command-0.1.4}/README.md +55 -26
- {python_base_command-0.1.2 → python_base_command-0.1.4}/pyproject.toml +2 -1
- {python_base_command-0.1.2 → python_base_command-0.1.4}/python_base_command/base.py +4 -15
- {python_base_command-0.1.2 → python_base_command-0.1.4}/python_base_command/runner.py +28 -22
- {python_base_command-0.1.2 → python_base_command-0.1.4}/usage_example/commands/greet.py +1 -0
- python_base_command-0.1.4/usage_example/commands/registry_cmd.py +35 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.config/README.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.config/black.toml +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.config/pylintrc +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.config/pylintrc_tests +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.config/ruff.toml +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/CODEOWNERS +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/chat-titles.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/claude-sonnet-4.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gemini-2.5-pro.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gpt-4.1.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gpt-4o.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gpt-5-mini.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gpt-5.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/nes-tab-completion.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/prompt.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/pull_request_template.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/workflows/lint.yml +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/workflows/publish_to_pypi.yml +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.github/workflows/tests.yml +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.gitignore +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/.pre-commit-config.yaml +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/CHANGELOG.md +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/LICENSE +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/MANIFEST.in +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/Taskfile.yml +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/cli.py +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/pytest.ini +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/python_base_command/__init__.py +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/python_base_command/registry.py +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/python_base_command/utils.py +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/tests/__init__.py +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/tests/test_base_command.py +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/usage_example/__init__.py +0 -0
- {python_base_command-0.1.2 → python_base_command-0.1.4}/usage_example/commands/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-base-command
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Django-style BaseCommand framework for standalone Python CLI tools
|
|
5
5
|
Project-URL: Homepage, https://github.com/aviz92/python-base-command
|
|
6
6
|
Project-URL: Repository, https://github.com/aviz92/python-base-command
|
|
@@ -18,6 +18,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Classifier: Topic :: Utilities
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
20
|
Requires-Dist: custom-python-logger>=2.0.13
|
|
21
|
+
Requires-Dist: python-base-toolkit>=1.0.2
|
|
21
22
|
Description-Content-Type: text/markdown
|
|
22
23
|
|
|
23
24
|

|
|
@@ -66,7 +67,7 @@ Start by creating `cli.py` — your entry point, the equivalent of Django's `man
|
|
|
66
67
|
|
|
67
68
|
```python
|
|
68
69
|
# cli.py
|
|
69
|
-
from
|
|
70
|
+
from python_base_command import Runner
|
|
70
71
|
|
|
71
72
|
Runner(commands_dir="commands").run()
|
|
72
73
|
```
|
|
@@ -83,11 +84,12 @@ myapp/
|
|
|
83
84
|
|
|
84
85
|
```python
|
|
85
86
|
# commands/greet.py
|
|
86
|
-
from
|
|
87
|
+
from python_base_command import BaseCommand, CommandError
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
class Command(BaseCommand):
|
|
90
91
|
help = "Greet a user by name"
|
|
92
|
+
version = "1.0.0"
|
|
91
93
|
|
|
92
94
|
def add_arguments(self, parser):
|
|
93
95
|
parser.add_argument("name", type=str, help="Name to greet")
|
|
@@ -108,21 +110,26 @@ class Command(BaseCommand):
|
|
|
108
110
|
Run from anywhere inside the project:
|
|
109
111
|
|
|
110
112
|
```bash
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
python3 cli.py --help # lists all available commands
|
|
114
|
+
python3 cli.py greet Alice
|
|
115
|
+
python3 cli.py greet Alice --shout
|
|
116
|
+
python3 cli.py greet --version
|
|
117
|
+
python3 cli.py greet --verbosity 2
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
---
|
|
119
121
|
|
|
120
122
|
## 📋 Manual Registry
|
|
121
123
|
|
|
122
|
-
Register commands explicitly using the `@registry.register()` decorator — useful when commands
|
|
124
|
+
Register commands explicitly using the `@registry.register()` decorator — useful when you want multiple commands in a single file.
|
|
125
|
+
|
|
126
|
+
The registry style works in two ways:
|
|
127
|
+
|
|
128
|
+
**Standalone** — run the registry directly as a script:
|
|
123
129
|
|
|
124
130
|
```python
|
|
125
|
-
|
|
131
|
+
# my_commands.py
|
|
132
|
+
from python_base_command import BaseCommand, CommandError, CommandRegistry
|
|
126
133
|
|
|
127
134
|
registry = CommandRegistry()
|
|
128
135
|
|
|
@@ -130,6 +137,7 @@ registry = CommandRegistry()
|
|
|
130
137
|
@registry.register("greet")
|
|
131
138
|
class GreetCommand(BaseCommand):
|
|
132
139
|
help = "Greet a user"
|
|
140
|
+
version = "2.0.0"
|
|
133
141
|
|
|
134
142
|
def add_arguments(self, parser):
|
|
135
143
|
parser.add_argument("name", type=str)
|
|
@@ -141,6 +149,7 @@ class GreetCommand(BaseCommand):
|
|
|
141
149
|
@registry.register("export")
|
|
142
150
|
class ExportCommand(BaseCommand):
|
|
143
151
|
help = "Export data"
|
|
152
|
+
version = "3.0.0"
|
|
144
153
|
|
|
145
154
|
def add_arguments(self, parser):
|
|
146
155
|
parser.add_argument("--format", choices=["csv", "json"], default="csv")
|
|
@@ -158,10 +167,26 @@ if __name__ == "__main__":
|
|
|
158
167
|
```
|
|
159
168
|
|
|
160
169
|
```bash
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
python3 my_commands.py greet Alice
|
|
171
|
+
python3 my_commands.py export --format json
|
|
172
|
+
python3 my_commands.py export --dry-run
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Auto-discovered** — drop the registry file into your `commands/` folder and `Runner` will discover it automatically alongside any classic `Command` files:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
myapp/
|
|
179
|
+
├── cli.py
|
|
180
|
+
└── commands/
|
|
181
|
+
├── __init__.py
|
|
182
|
+
├── greet.py ← classic Command class
|
|
183
|
+
└── reg_cmd.py ← CommandRegistry with multiple commands
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python3 cli.py --help # shows commands from both files
|
|
188
|
+
python3 cli.py greet Alice
|
|
189
|
+
python3 cli.py export --format json
|
|
165
190
|
```
|
|
166
191
|
|
|
167
192
|
---
|
|
@@ -171,7 +196,7 @@ python cli.py export --dry-run
|
|
|
171
196
|
Invoke commands programmatically — ideal for unit tests.
|
|
172
197
|
|
|
173
198
|
```python
|
|
174
|
-
from
|
|
199
|
+
from python_base_command import call_command, CommandError
|
|
175
200
|
import pytest
|
|
176
201
|
|
|
177
202
|
from commands.greet import Command as GreetCommand
|
|
@@ -202,6 +227,7 @@ Base class for all commands. Inherit from it and implement `handle()`.
|
|
|
202
227
|
| Attribute | Type | Default | Description |
|
|
203
228
|
|---|---|---|---|
|
|
204
229
|
| `help` | `str` | `""` | Description shown in `--help` |
|
|
230
|
+
| `version` | `str` | `"unknown"` | Version string exposed via `--version`. Set this per command. |
|
|
205
231
|
| `output_transaction` | `bool` | `False` | Wrap `handle()` return value in `BEGIN;` / `COMMIT;` |
|
|
206
232
|
| `suppressed_base_arguments` | `set[str]` | `set()` | Base flags to hide from `--help` |
|
|
207
233
|
| `stealth_options` | `tuple[str]` | `()` | Options used but not declared via `add_arguments()` |
|
|
@@ -213,7 +239,6 @@ Base class for all commands. Inherit from it and implement `handle()`.
|
|
|
213
239
|
|---|---|---|
|
|
214
240
|
| `handle(**kwargs)` | ✅ | Command logic. May return a string. |
|
|
215
241
|
| `add_arguments(parser)` | ❌ | Add command-specific arguments to the parser. |
|
|
216
|
-
| `get_version()` | ❌ | Override to expose your package version via `--version`. |
|
|
217
242
|
|
|
218
243
|
**`self.logger`**
|
|
219
244
|
|
|
@@ -255,7 +280,7 @@ raise CommandError("Fatal error.", returncode=2)
|
|
|
255
280
|
For commands that accept one or more arbitrary string labels. Override `handle_label()` instead of `handle()`.
|
|
256
281
|
|
|
257
282
|
```python
|
|
258
|
-
from
|
|
283
|
+
from python_base_command import LabelCommand, CommandError
|
|
259
284
|
|
|
260
285
|
|
|
261
286
|
class Command(LabelCommand):
|
|
@@ -278,24 +303,27 @@ class Command(LabelCommand):
|
|
|
278
303
|
```
|
|
279
304
|
|
|
280
305
|
```bash
|
|
281
|
-
|
|
282
|
-
|
|
306
|
+
python3 cli.py process report.csv notes.txt image.png
|
|
307
|
+
python3 cli.py process report.csv notes.txt image.png --strict
|
|
283
308
|
```
|
|
284
309
|
|
|
285
310
|
---
|
|
286
311
|
|
|
287
312
|
### `Runner`
|
|
288
313
|
|
|
289
|
-
Auto-discovers commands from a directory.
|
|
314
|
+
Auto-discovers commands from a directory. Two conventions are supported:
|
|
315
|
+
|
|
316
|
+
1. **Classic** — a `.py` file that defines a class named `Command` subclassing `BaseCommand`. The command name is the file stem.
|
|
317
|
+
2. **Registry** — a `.py` file that defines one or more `CommandRegistry` instances. Every command registered on those instances is merged in automatically; command names come from the registry, not the file name.
|
|
318
|
+
|
|
319
|
+
Files whose names start with `_` are ignored.
|
|
290
320
|
|
|
291
321
|
```python
|
|
292
|
-
from
|
|
322
|
+
from python_base_command import Runner
|
|
293
323
|
|
|
294
324
|
Runner(commands_dir="commands").run()
|
|
295
325
|
```
|
|
296
326
|
|
|
297
|
-
Files whose names start with `_` are ignored.
|
|
298
|
-
|
|
299
327
|
---
|
|
300
328
|
|
|
301
329
|
### `CommandRegistry`
|
|
@@ -303,17 +331,19 @@ Files whose names start with `_` are ignored.
|
|
|
303
331
|
Manually register commands using a decorator or programmatically.
|
|
304
332
|
|
|
305
333
|
```python
|
|
306
|
-
from
|
|
334
|
+
from python_base_command import BaseCommand, CommandRegistry
|
|
307
335
|
|
|
308
336
|
registry = CommandRegistry()
|
|
309
337
|
|
|
338
|
+
|
|
310
339
|
@registry.register("greet")
|
|
311
340
|
class GreetCommand(BaseCommand): ...
|
|
312
341
|
|
|
342
|
+
|
|
313
343
|
registry.add("export", ExportCommand) # programmatic alternative
|
|
314
344
|
|
|
315
|
-
registry.run()
|
|
316
|
-
registry.run(["myapp", "greet", "Alice"])
|
|
345
|
+
registry.run() # uses sys.argv
|
|
346
|
+
registry.run(["myapp", "greet", "Alice"]) # explicit argv
|
|
317
347
|
```
|
|
318
348
|
|
|
319
349
|
---
|
|
@@ -323,7 +353,7 @@ registry.run(["myapp", "greet", "Alice"]) # explicit argv
|
|
|
323
353
|
Invoke a command from Python code. Accepts either a class or an instance.
|
|
324
354
|
|
|
325
355
|
```python
|
|
326
|
-
from
|
|
356
|
+
from python_base_command import call_command
|
|
327
357
|
|
|
328
358
|
call_command(GreetCommand, name="Alice")
|
|
329
359
|
call_command(GreetCommand, name="Alice", verbosity=0)
|
|
@@ -44,7 +44,7 @@ Start by creating `cli.py` — your entry point, the equivalent of Django's `man
|
|
|
44
44
|
|
|
45
45
|
```python
|
|
46
46
|
# cli.py
|
|
47
|
-
from
|
|
47
|
+
from python_base_command import Runner
|
|
48
48
|
|
|
49
49
|
Runner(commands_dir="commands").run()
|
|
50
50
|
```
|
|
@@ -61,11 +61,12 @@ myapp/
|
|
|
61
61
|
|
|
62
62
|
```python
|
|
63
63
|
# commands/greet.py
|
|
64
|
-
from
|
|
64
|
+
from python_base_command import BaseCommand, CommandError
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class Command(BaseCommand):
|
|
68
68
|
help = "Greet a user by name"
|
|
69
|
+
version = "1.0.0"
|
|
69
70
|
|
|
70
71
|
def add_arguments(self, parser):
|
|
71
72
|
parser.add_argument("name", type=str, help="Name to greet")
|
|
@@ -86,21 +87,26 @@ class Command(BaseCommand):
|
|
|
86
87
|
Run from anywhere inside the project:
|
|
87
88
|
|
|
88
89
|
```bash
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
python3 cli.py --help # lists all available commands
|
|
91
|
+
python3 cli.py greet Alice
|
|
92
|
+
python3 cli.py greet Alice --shout
|
|
93
|
+
python3 cli.py greet --version
|
|
94
|
+
python3 cli.py greet --verbosity 2
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
---
|
|
97
98
|
|
|
98
99
|
## 📋 Manual Registry
|
|
99
100
|
|
|
100
|
-
Register commands explicitly using the `@registry.register()` decorator — useful when commands
|
|
101
|
+
Register commands explicitly using the `@registry.register()` decorator — useful when you want multiple commands in a single file.
|
|
102
|
+
|
|
103
|
+
The registry style works in two ways:
|
|
104
|
+
|
|
105
|
+
**Standalone** — run the registry directly as a script:
|
|
101
106
|
|
|
102
107
|
```python
|
|
103
|
-
|
|
108
|
+
# my_commands.py
|
|
109
|
+
from python_base_command import BaseCommand, CommandError, CommandRegistry
|
|
104
110
|
|
|
105
111
|
registry = CommandRegistry()
|
|
106
112
|
|
|
@@ -108,6 +114,7 @@ registry = CommandRegistry()
|
|
|
108
114
|
@registry.register("greet")
|
|
109
115
|
class GreetCommand(BaseCommand):
|
|
110
116
|
help = "Greet a user"
|
|
117
|
+
version = "2.0.0"
|
|
111
118
|
|
|
112
119
|
def add_arguments(self, parser):
|
|
113
120
|
parser.add_argument("name", type=str)
|
|
@@ -119,6 +126,7 @@ class GreetCommand(BaseCommand):
|
|
|
119
126
|
@registry.register("export")
|
|
120
127
|
class ExportCommand(BaseCommand):
|
|
121
128
|
help = "Export data"
|
|
129
|
+
version = "3.0.0"
|
|
122
130
|
|
|
123
131
|
def add_arguments(self, parser):
|
|
124
132
|
parser.add_argument("--format", choices=["csv", "json"], default="csv")
|
|
@@ -136,10 +144,26 @@ if __name__ == "__main__":
|
|
|
136
144
|
```
|
|
137
145
|
|
|
138
146
|
```bash
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
python3 my_commands.py greet Alice
|
|
148
|
+
python3 my_commands.py export --format json
|
|
149
|
+
python3 my_commands.py export --dry-run
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Auto-discovered** — drop the registry file into your `commands/` folder and `Runner` will discover it automatically alongside any classic `Command` files:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
myapp/
|
|
156
|
+
├── cli.py
|
|
157
|
+
└── commands/
|
|
158
|
+
├── __init__.py
|
|
159
|
+
├── greet.py ← classic Command class
|
|
160
|
+
└── reg_cmd.py ← CommandRegistry with multiple commands
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
python3 cli.py --help # shows commands from both files
|
|
165
|
+
python3 cli.py greet Alice
|
|
166
|
+
python3 cli.py export --format json
|
|
143
167
|
```
|
|
144
168
|
|
|
145
169
|
---
|
|
@@ -149,7 +173,7 @@ python cli.py export --dry-run
|
|
|
149
173
|
Invoke commands programmatically — ideal for unit tests.
|
|
150
174
|
|
|
151
175
|
```python
|
|
152
|
-
from
|
|
176
|
+
from python_base_command import call_command, CommandError
|
|
153
177
|
import pytest
|
|
154
178
|
|
|
155
179
|
from commands.greet import Command as GreetCommand
|
|
@@ -180,6 +204,7 @@ Base class for all commands. Inherit from it and implement `handle()`.
|
|
|
180
204
|
| Attribute | Type | Default | Description |
|
|
181
205
|
|---|---|---|---|
|
|
182
206
|
| `help` | `str` | `""` | Description shown in `--help` |
|
|
207
|
+
| `version` | `str` | `"unknown"` | Version string exposed via `--version`. Set this per command. |
|
|
183
208
|
| `output_transaction` | `bool` | `False` | Wrap `handle()` return value in `BEGIN;` / `COMMIT;` |
|
|
184
209
|
| `suppressed_base_arguments` | `set[str]` | `set()` | Base flags to hide from `--help` |
|
|
185
210
|
| `stealth_options` | `tuple[str]` | `()` | Options used but not declared via `add_arguments()` |
|
|
@@ -191,7 +216,6 @@ Base class for all commands. Inherit from it and implement `handle()`.
|
|
|
191
216
|
|---|---|---|
|
|
192
217
|
| `handle(**kwargs)` | ✅ | Command logic. May return a string. |
|
|
193
218
|
| `add_arguments(parser)` | ❌ | Add command-specific arguments to the parser. |
|
|
194
|
-
| `get_version()` | ❌ | Override to expose your package version via `--version`. |
|
|
195
219
|
|
|
196
220
|
**`self.logger`**
|
|
197
221
|
|
|
@@ -233,7 +257,7 @@ raise CommandError("Fatal error.", returncode=2)
|
|
|
233
257
|
For commands that accept one or more arbitrary string labels. Override `handle_label()` instead of `handle()`.
|
|
234
258
|
|
|
235
259
|
```python
|
|
236
|
-
from
|
|
260
|
+
from python_base_command import LabelCommand, CommandError
|
|
237
261
|
|
|
238
262
|
|
|
239
263
|
class Command(LabelCommand):
|
|
@@ -256,24 +280,27 @@ class Command(LabelCommand):
|
|
|
256
280
|
```
|
|
257
281
|
|
|
258
282
|
```bash
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
python3 cli.py process report.csv notes.txt image.png
|
|
284
|
+
python3 cli.py process report.csv notes.txt image.png --strict
|
|
261
285
|
```
|
|
262
286
|
|
|
263
287
|
---
|
|
264
288
|
|
|
265
289
|
### `Runner`
|
|
266
290
|
|
|
267
|
-
Auto-discovers commands from a directory.
|
|
291
|
+
Auto-discovers commands from a directory. Two conventions are supported:
|
|
292
|
+
|
|
293
|
+
1. **Classic** — a `.py` file that defines a class named `Command` subclassing `BaseCommand`. The command name is the file stem.
|
|
294
|
+
2. **Registry** — a `.py` file that defines one or more `CommandRegistry` instances. Every command registered on those instances is merged in automatically; command names come from the registry, not the file name.
|
|
295
|
+
|
|
296
|
+
Files whose names start with `_` are ignored.
|
|
268
297
|
|
|
269
298
|
```python
|
|
270
|
-
from
|
|
299
|
+
from python_base_command import Runner
|
|
271
300
|
|
|
272
301
|
Runner(commands_dir="commands").run()
|
|
273
302
|
```
|
|
274
303
|
|
|
275
|
-
Files whose names start with `_` are ignored.
|
|
276
|
-
|
|
277
304
|
---
|
|
278
305
|
|
|
279
306
|
### `CommandRegistry`
|
|
@@ -281,17 +308,19 @@ Files whose names start with `_` are ignored.
|
|
|
281
308
|
Manually register commands using a decorator or programmatically.
|
|
282
309
|
|
|
283
310
|
```python
|
|
284
|
-
from
|
|
311
|
+
from python_base_command import BaseCommand, CommandRegistry
|
|
285
312
|
|
|
286
313
|
registry = CommandRegistry()
|
|
287
314
|
|
|
315
|
+
|
|
288
316
|
@registry.register("greet")
|
|
289
317
|
class GreetCommand(BaseCommand): ...
|
|
290
318
|
|
|
319
|
+
|
|
291
320
|
registry.add("export", ExportCommand) # programmatic alternative
|
|
292
321
|
|
|
293
|
-
registry.run()
|
|
294
|
-
registry.run(["myapp", "greet", "Alice"])
|
|
322
|
+
registry.run() # uses sys.argv
|
|
323
|
+
registry.run(["myapp", "greet", "Alice"]) # explicit argv
|
|
295
324
|
```
|
|
296
325
|
|
|
297
326
|
---
|
|
@@ -301,7 +330,7 @@ registry.run(["myapp", "greet", "Alice"]) # explicit argv
|
|
|
301
330
|
Invoke a command from Python code. Accepts either a class or an instance.
|
|
302
331
|
|
|
303
332
|
```python
|
|
304
|
-
from
|
|
333
|
+
from python_base_command import call_command
|
|
305
334
|
|
|
306
335
|
call_command(GreetCommand, name="Alice")
|
|
307
336
|
call_command(GreetCommand, name="Alice", verbosity=0)
|
|
@@ -4,13 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-base-command"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Django-style BaseCommand framework for standalone Python CLI tools"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.12"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"custom-python-logger>=2.0.13",
|
|
14
|
+
"python-base-toolkit>=1.0.2",
|
|
14
15
|
]
|
|
15
16
|
keywords = ["cli", "command", "argparse", "django", "management"]
|
|
16
17
|
classifiers = [
|
|
@@ -6,7 +6,6 @@ replacing self.stdout / self.style with self.logger from custom-python-logger.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
|
-
import importlib.metadata
|
|
10
9
|
import os
|
|
11
10
|
import sys
|
|
12
11
|
from argparse import Action, ArgumentParser, HelpFormatter
|
|
@@ -142,6 +141,8 @@ class BaseCommand:
|
|
|
142
141
|
----------
|
|
143
142
|
help : str
|
|
144
143
|
Short description printed in --help output.
|
|
144
|
+
version : str
|
|
145
|
+
Version string exposed via --version. Set this per command.
|
|
145
146
|
output_transaction : bool
|
|
146
147
|
If True, wrap any string returned by handle() with BEGIN; / COMMIT;.
|
|
147
148
|
suppressed_base_arguments : set[str]
|
|
@@ -153,6 +154,7 @@ class BaseCommand:
|
|
|
153
154
|
"""
|
|
154
155
|
|
|
155
156
|
help: str = ""
|
|
157
|
+
version: str = "unknown"
|
|
156
158
|
output_transaction: bool = False
|
|
157
159
|
suppressed_base_arguments: set[str] = set()
|
|
158
160
|
stealth_options: tuple[str, ...] = ()
|
|
@@ -170,19 +172,6 @@ class BaseCommand:
|
|
|
170
172
|
_ = stdout, stderr # API compatibility with call_command(stdout=..., stderr=...)
|
|
171
173
|
self.logger: CustomLoggerAdapter = get_logger(name=self.__class__.__module__.split(".", maxsplit=1)[0])
|
|
172
174
|
|
|
173
|
-
# ------------------------------------------------------------------ version
|
|
174
|
-
|
|
175
|
-
def get_version(self) -> str:
|
|
176
|
-
"""
|
|
177
|
-
Return the version string for this command.
|
|
178
|
-
Override to expose your own application version via --version.
|
|
179
|
-
"""
|
|
180
|
-
try:
|
|
181
|
-
pkg = self.__module__.split(".", maxsplit=1)[0]
|
|
182
|
-
return importlib.metadata.version(pkg)
|
|
183
|
-
except Exception:
|
|
184
|
-
return "unknown"
|
|
185
|
-
|
|
186
175
|
# ------------------------------------------------------------------ parser
|
|
187
176
|
|
|
188
177
|
def create_parser(self, prog_name: str, subcommand: str, **kwargs: Any) -> CommandParser:
|
|
@@ -200,7 +189,7 @@ class BaseCommand:
|
|
|
200
189
|
parser,
|
|
201
190
|
"--version",
|
|
202
191
|
action="version",
|
|
203
|
-
version=self.
|
|
192
|
+
version=self.version,
|
|
204
193
|
help="Show program's version number and exit.",
|
|
205
194
|
)
|
|
206
195
|
self.add_base_argument(
|
|
@@ -40,15 +40,14 @@ And run::
|
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
42
|
import importlib.util
|
|
43
|
-
import os
|
|
44
43
|
import sys
|
|
45
44
|
from pathlib import Path
|
|
46
45
|
from types import ModuleType
|
|
47
|
-
from typing import Optional
|
|
48
46
|
|
|
49
47
|
from custom_python_logger import get_logger
|
|
50
48
|
|
|
51
|
-
from .base import BaseCommand
|
|
49
|
+
from python_base_command.base import BaseCommand
|
|
50
|
+
from python_base_command.registry import CommandRegistry
|
|
52
51
|
|
|
53
52
|
logger = get_logger("python-base-command")
|
|
54
53
|
|
|
@@ -69,7 +68,7 @@ class Runner:
|
|
|
69
68
|
def __init__(
|
|
70
69
|
self,
|
|
71
70
|
commands_dir: str | Path = "commands",
|
|
72
|
-
):
|
|
71
|
+
) -> None:
|
|
73
72
|
# Resolve relative to cwd — the directory the user runs the script from,
|
|
74
73
|
# just like Django resolves manage.py commands from the project root.
|
|
75
74
|
self._commands_dir = (Path.cwd() / commands_dir).resolve()
|
|
@@ -78,8 +77,15 @@ class Runner:
|
|
|
78
77
|
|
|
79
78
|
def _discover(self) -> dict[str, type[BaseCommand]]:
|
|
80
79
|
"""
|
|
81
|
-
Walk ``self._commands_dir`` and import every non-private module
|
|
82
|
-
|
|
80
|
+
Walk ``self._commands_dir`` and import every non-private module.
|
|
81
|
+
|
|
82
|
+
Two conventions are supported per module:
|
|
83
|
+
|
|
84
|
+
1. **Classic** — a class literally named ``Command`` that subclasses
|
|
85
|
+
``BaseCommand``. The command name is the module's file stem.
|
|
86
|
+
2. **Registry** — one or more ``CommandRegistry`` instances defined at
|
|
87
|
+
module level. Every command registered on those instances is merged
|
|
88
|
+
in; the names come from the registry (not the file stem).
|
|
83
89
|
"""
|
|
84
90
|
commands: dict[str, type[BaseCommand]] = {}
|
|
85
91
|
|
|
@@ -90,24 +96,26 @@ class Runner:
|
|
|
90
96
|
if path.stem.startswith("_"):
|
|
91
97
|
continue
|
|
92
98
|
|
|
93
|
-
module
|
|
94
|
-
if module is None:
|
|
99
|
+
if (module := self._load_module(path)) is None:
|
|
95
100
|
continue
|
|
96
101
|
|
|
102
|
+
# --- 1. Classic: a top-level class named "Command" ---
|
|
97
103
|
command_class = getattr(module, "Command", None)
|
|
98
|
-
if command_class is None:
|
|
99
|
-
|
|
100
|
-
if not (
|
|
101
|
-
isinstance(command_class, type) and issubclass(command_class, BaseCommand)
|
|
102
|
-
):
|
|
103
|
-
continue
|
|
104
|
+
if command_class is not None and isinstance(command_class, type) and issubclass(command_class, BaseCommand):
|
|
105
|
+
commands[path.stem] = command_class
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
# --- 2. Registry: any CommandRegistry instances in the module ---
|
|
108
|
+
for attr_name in dir(module):
|
|
109
|
+
obj = getattr(module, attr_name)
|
|
110
|
+
if isinstance(obj, CommandRegistry):
|
|
111
|
+
for name in obj.list_commands():
|
|
112
|
+
if (cls := obj.get(name)) is not None:
|
|
113
|
+
commands[name] = cls
|
|
106
114
|
|
|
107
115
|
return commands
|
|
108
116
|
|
|
109
117
|
@staticmethod
|
|
110
|
-
def _load_module(path: Path) ->
|
|
118
|
+
def _load_module(path: Path) -> ModuleType | None:
|
|
111
119
|
"""Dynamically load a Python file as a module."""
|
|
112
120
|
module_name = f"_base_command_discovered_.{path.stem}"
|
|
113
121
|
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
@@ -123,7 +131,7 @@ class Runner:
|
|
|
123
131
|
|
|
124
132
|
# ------------------------------------------------------------------ running
|
|
125
133
|
|
|
126
|
-
def run(self, argv: list[str] | None = None):
|
|
134
|
+
def run(self, argv: list[str] | None = None) -> None:
|
|
127
135
|
"""
|
|
128
136
|
Parse *argv* (defaults to ``sys.argv``), discover commands, find the
|
|
129
137
|
requested one, and run it.
|
|
@@ -132,14 +140,12 @@ class Runner:
|
|
|
132
140
|
commands = self._discover()
|
|
133
141
|
|
|
134
142
|
# Show top-level help if no subcommand is given.
|
|
135
|
-
if len(argv) < 2 or argv[1] in
|
|
143
|
+
if len(argv) < 2 or argv[1] in {"-h", "--help"}:
|
|
136
144
|
self._print_help(argv[0] if argv else "unknown", commands)
|
|
137
145
|
sys.exit(0)
|
|
138
146
|
|
|
139
147
|
subcommand = argv[1]
|
|
140
|
-
command_class
|
|
141
|
-
|
|
142
|
-
if command_class is None:
|
|
148
|
+
if (command_class := commands.get(subcommand)) is None:
|
|
143
149
|
prog = argv[0] if argv else "unknown"
|
|
144
150
|
available = ", ".join(sorted(commands)) or "(none found)"
|
|
145
151
|
logger.error(
|
|
@@ -153,7 +159,7 @@ class Runner:
|
|
|
153
159
|
command_class().run_from_argv([argv[0]] + argv[2:])
|
|
154
160
|
|
|
155
161
|
@staticmethod
|
|
156
|
-
def _print_help(prog: str, commands: dict[str, type[BaseCommand]]):
|
|
162
|
+
def _print_help(prog: str, commands: dict[str, type[BaseCommand]]) -> None:
|
|
157
163
|
print(f"Usage: {prog} <command> [options]\n")
|
|
158
164
|
print("Available commands:")
|
|
159
165
|
for name, cls in sorted(commands.items()):
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from python_base_command import BaseCommand, CommandParser, CommandRegistry
|
|
4
|
+
|
|
5
|
+
registry = CommandRegistry()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@registry.register("greet2")
|
|
9
|
+
class Greet2Command(BaseCommand):
|
|
10
|
+
help = "Greet a user"
|
|
11
|
+
|
|
12
|
+
def add_arguments(self, parser: CommandParser) -> None:
|
|
13
|
+
parser.add_argument("name", type=str)
|
|
14
|
+
|
|
15
|
+
def handle(self, **kwargs: Any) -> None:
|
|
16
|
+
self.logger.info(f"Hello, {kwargs['name']}!")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@registry.register("export")
|
|
20
|
+
class ExportCommand(BaseCommand):
|
|
21
|
+
help = "Export data"
|
|
22
|
+
|
|
23
|
+
def add_arguments(self, parser: CommandParser) -> None:
|
|
24
|
+
parser.add_argument("--format", choices=["csv", "json"], default="csv")
|
|
25
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
26
|
+
|
|
27
|
+
def handle(self, **kwargs: Any) -> None:
|
|
28
|
+
if kwargs["dry_run"]:
|
|
29
|
+
self.logger.warning("Dry run — no files written.")
|
|
30
|
+
return
|
|
31
|
+
self.logger.info(f"Exported as {kwargs['format']}.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
registry.run()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_base_command-0.1.2 → python_base_command-0.1.4}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gpt-4.1.md
RENAMED
|
File without changes
|
{python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gpt-4o.md
RENAMED
|
File without changes
|
{python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gpt-5-mini.md
RENAMED
|
File without changes
|
{python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/gpt-5.md
RENAMED
|
File without changes
|
|
File without changes
|
{python_base_command-0.1.2 → python_base_command-0.1.4}/.github/instructions/IDE Agent/prompt.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_base_command-0.1.2 → python_base_command-0.1.4}/.github/workflows/publish_to_pypi.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|