python-base-command 0.1.3__tar.gz → 0.1.5__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.3 → python_base_command-0.1.5}/PKG-INFO +76 -27
- {python_base_command-0.1.3 → python_base_command-0.1.5}/README.md +74 -26
- {python_base_command-0.1.3 → python_base_command-0.1.5}/pyproject.toml +2 -1
- {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/__init__.py +0 -7
- {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/base.py +21 -17
- {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/runner.py +10 -16
- {python_base_command-0.1.3 → python_base_command-0.1.5}/usage_example/commands/greet.py +1 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/usage_example/commands/registry_cmd.py +13 -5
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/README.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/black.toml +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/pylintrc +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/pylintrc_tests +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/ruff.toml +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/CODEOWNERS +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/chat-titles.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/claude-sonnet-4.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gemini-2.5-pro.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-4.1.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-4o.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-5-mini.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-5.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/nes-tab-completion.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/prompt.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/pull_request_template.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/workflows/lint.yml +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/workflows/publish_to_pypi.yml +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/workflows/tests.yml +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.gitignore +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/.pre-commit-config.yaml +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/CHANGELOG.md +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/LICENSE +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/MANIFEST.in +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/Taskfile.yml +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/cli.py +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/pytest.ini +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/registry.py +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/utils.py +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/tests/__init__.py +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/tests/test_base_command.py +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/usage_example/__init__.py +0 -0
- {python_base_command-0.1.3 → python_base_command-0.1.5}/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.5
|
|
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")
|
|
@@ -105,24 +107,48 @@ class Command(BaseCommand):
|
|
|
105
107
|
self.logger.info(msg)
|
|
106
108
|
```
|
|
107
109
|
|
|
110
|
+
```python
|
|
111
|
+
# commands/greet.py
|
|
112
|
+
from python_base_command import BaseCommand, CommandError
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Command(BaseCommand):
|
|
116
|
+
help = "Greet a user by name"
|
|
117
|
+
|
|
118
|
+
def __init__(self) -> None:
|
|
119
|
+
super().__init__()
|
|
120
|
+
self.set_project_version("python-base-command")
|
|
121
|
+
|
|
122
|
+
def add_arguments(self, parser):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def handle(self, **kwargs):
|
|
126
|
+
pass
|
|
127
|
+
```
|
|
128
|
+
|
|
108
129
|
Run from anywhere inside the project:
|
|
109
130
|
|
|
110
131
|
```bash
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
python3 cli.py --help # lists all available commands
|
|
133
|
+
python3 cli.py greet Alice
|
|
134
|
+
python3 cli.py greet Alice --shout
|
|
135
|
+
python3 cli.py greet --version
|
|
136
|
+
python3 cli.py greet --verbosity 2
|
|
116
137
|
```
|
|
117
138
|
|
|
118
139
|
---
|
|
119
140
|
|
|
120
141
|
## 📋 Manual Registry
|
|
121
142
|
|
|
122
|
-
Register commands explicitly using the `@registry.register()` decorator — useful when commands
|
|
143
|
+
Register commands explicitly using the `@registry.register()` decorator — useful when you want multiple commands in a single file.
|
|
144
|
+
|
|
145
|
+
The registry style works in two ways:
|
|
146
|
+
|
|
147
|
+
**Standalone** — run the registry directly as a script:
|
|
123
148
|
|
|
124
149
|
```python
|
|
125
|
-
|
|
150
|
+
# my_commands.py
|
|
151
|
+
from python_base_command import BaseCommand, CommandError, CommandRegistry
|
|
126
152
|
|
|
127
153
|
registry = CommandRegistry()
|
|
128
154
|
|
|
@@ -130,6 +156,7 @@ registry = CommandRegistry()
|
|
|
130
156
|
@registry.register("greet")
|
|
131
157
|
class GreetCommand(BaseCommand):
|
|
132
158
|
help = "Greet a user"
|
|
159
|
+
version = "2.0.0"
|
|
133
160
|
|
|
134
161
|
def add_arguments(self, parser):
|
|
135
162
|
parser.add_argument("name", type=str)
|
|
@@ -141,6 +168,7 @@ class GreetCommand(BaseCommand):
|
|
|
141
168
|
@registry.register("export")
|
|
142
169
|
class ExportCommand(BaseCommand):
|
|
143
170
|
help = "Export data"
|
|
171
|
+
version = "3.0.0"
|
|
144
172
|
|
|
145
173
|
def add_arguments(self, parser):
|
|
146
174
|
parser.add_argument("--format", choices=["csv", "json"], default="csv")
|
|
@@ -158,10 +186,26 @@ if __name__ == "__main__":
|
|
|
158
186
|
```
|
|
159
187
|
|
|
160
188
|
```bash
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
189
|
+
python3 my_commands.py greet Alice
|
|
190
|
+
python3 my_commands.py export --format json
|
|
191
|
+
python3 my_commands.py export --dry-run
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Auto-discovered** — drop the registry file into your `commands/` folder and `Runner` will discover it automatically alongside any classic `Command` files:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
myapp/
|
|
198
|
+
├── cli.py
|
|
199
|
+
└── commands/
|
|
200
|
+
├── __init__.py
|
|
201
|
+
├── greet.py ← classic Command class
|
|
202
|
+
└── reg_cmd.py ← CommandRegistry with multiple commands
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
python3 cli.py --help # shows commands from both files
|
|
207
|
+
python3 cli.py greet Alice
|
|
208
|
+
python3 cli.py export --format json
|
|
165
209
|
```
|
|
166
210
|
|
|
167
211
|
---
|
|
@@ -171,7 +215,7 @@ python cli.py export --dry-run
|
|
|
171
215
|
Invoke commands programmatically — ideal for unit tests.
|
|
172
216
|
|
|
173
217
|
```python
|
|
174
|
-
from
|
|
218
|
+
from python_base_command import call_command, CommandError
|
|
175
219
|
import pytest
|
|
176
220
|
|
|
177
221
|
from commands.greet import Command as GreetCommand
|
|
@@ -202,6 +246,7 @@ Base class for all commands. Inherit from it and implement `handle()`.
|
|
|
202
246
|
| Attribute | Type | Default | Description |
|
|
203
247
|
|---|---|---|---|
|
|
204
248
|
| `help` | `str` | `""` | Description shown in `--help` |
|
|
249
|
+
| `version` | `str` | `"unknown"` | Version string exposed via `--version`. Set this per command. |
|
|
205
250
|
| `output_transaction` | `bool` | `False` | Wrap `handle()` return value in `BEGIN;` / `COMMIT;` |
|
|
206
251
|
| `suppressed_base_arguments` | `set[str]` | `set()` | Base flags to hide from `--help` |
|
|
207
252
|
| `stealth_options` | `tuple[str]` | `()` | Options used but not declared via `add_arguments()` |
|
|
@@ -213,7 +258,6 @@ Base class for all commands. Inherit from it and implement `handle()`.
|
|
|
213
258
|
|---|---|---|
|
|
214
259
|
| `handle(**kwargs)` | ✅ | Command logic. May return a string. |
|
|
215
260
|
| `add_arguments(parser)` | ❌ | Add command-specific arguments to the parser. |
|
|
216
|
-
| `get_version()` | ❌ | Override to expose your package version via `--version`. |
|
|
217
261
|
|
|
218
262
|
**`self.logger`**
|
|
219
263
|
|
|
@@ -255,7 +299,7 @@ raise CommandError("Fatal error.", returncode=2)
|
|
|
255
299
|
For commands that accept one or more arbitrary string labels. Override `handle_label()` instead of `handle()`.
|
|
256
300
|
|
|
257
301
|
```python
|
|
258
|
-
from
|
|
302
|
+
from python_base_command import LabelCommand, CommandError
|
|
259
303
|
|
|
260
304
|
|
|
261
305
|
class Command(LabelCommand):
|
|
@@ -278,24 +322,27 @@ class Command(LabelCommand):
|
|
|
278
322
|
```
|
|
279
323
|
|
|
280
324
|
```bash
|
|
281
|
-
|
|
282
|
-
|
|
325
|
+
python3 cli.py process report.csv notes.txt image.png
|
|
326
|
+
python3 cli.py process report.csv notes.txt image.png --strict
|
|
283
327
|
```
|
|
284
328
|
|
|
285
329
|
---
|
|
286
330
|
|
|
287
331
|
### `Runner`
|
|
288
332
|
|
|
289
|
-
Auto-discovers commands from a directory.
|
|
333
|
+
Auto-discovers commands from a directory. Two conventions are supported:
|
|
334
|
+
|
|
335
|
+
1. **Classic** — a `.py` file that defines a class named `Command` subclassing `BaseCommand`. The command name is the file stem.
|
|
336
|
+
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.
|
|
337
|
+
|
|
338
|
+
Files whose names start with `_` are ignored.
|
|
290
339
|
|
|
291
340
|
```python
|
|
292
|
-
from
|
|
341
|
+
from python_base_command import Runner
|
|
293
342
|
|
|
294
343
|
Runner(commands_dir="commands").run()
|
|
295
344
|
```
|
|
296
345
|
|
|
297
|
-
Files whose names start with `_` are ignored.
|
|
298
|
-
|
|
299
346
|
---
|
|
300
347
|
|
|
301
348
|
### `CommandRegistry`
|
|
@@ -303,17 +350,19 @@ Files whose names start with `_` are ignored.
|
|
|
303
350
|
Manually register commands using a decorator or programmatically.
|
|
304
351
|
|
|
305
352
|
```python
|
|
306
|
-
from
|
|
353
|
+
from python_base_command import BaseCommand, CommandRegistry
|
|
307
354
|
|
|
308
355
|
registry = CommandRegistry()
|
|
309
356
|
|
|
357
|
+
|
|
310
358
|
@registry.register("greet")
|
|
311
359
|
class GreetCommand(BaseCommand): ...
|
|
312
360
|
|
|
361
|
+
|
|
313
362
|
registry.add("export", ExportCommand) # programmatic alternative
|
|
314
363
|
|
|
315
|
-
registry.run()
|
|
316
|
-
registry.run(["myapp", "greet", "Alice"])
|
|
364
|
+
registry.run() # uses sys.argv
|
|
365
|
+
registry.run(["myapp", "greet", "Alice"]) # explicit argv
|
|
317
366
|
```
|
|
318
367
|
|
|
319
368
|
---
|
|
@@ -323,7 +372,7 @@ registry.run(["myapp", "greet", "Alice"]) # explicit argv
|
|
|
323
372
|
Invoke a command from Python code. Accepts either a class or an instance.
|
|
324
373
|
|
|
325
374
|
```python
|
|
326
|
-
from
|
|
375
|
+
from python_base_command import call_command
|
|
327
376
|
|
|
328
377
|
call_command(GreetCommand, name="Alice")
|
|
329
378
|
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")
|
|
@@ -83,24 +84,48 @@ class Command(BaseCommand):
|
|
|
83
84
|
self.logger.info(msg)
|
|
84
85
|
```
|
|
85
86
|
|
|
87
|
+
```python
|
|
88
|
+
# commands/greet.py
|
|
89
|
+
from python_base_command import BaseCommand, CommandError
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Command(BaseCommand):
|
|
93
|
+
help = "Greet a user by name"
|
|
94
|
+
|
|
95
|
+
def __init__(self) -> None:
|
|
96
|
+
super().__init__()
|
|
97
|
+
self.set_project_version("python-base-command")
|
|
98
|
+
|
|
99
|
+
def add_arguments(self, parser):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def handle(self, **kwargs):
|
|
103
|
+
pass
|
|
104
|
+
```
|
|
105
|
+
|
|
86
106
|
Run from anywhere inside the project:
|
|
87
107
|
|
|
88
108
|
```bash
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
python3 cli.py --help # lists all available commands
|
|
110
|
+
python3 cli.py greet Alice
|
|
111
|
+
python3 cli.py greet Alice --shout
|
|
112
|
+
python3 cli.py greet --version
|
|
113
|
+
python3 cli.py greet --verbosity 2
|
|
94
114
|
```
|
|
95
115
|
|
|
96
116
|
---
|
|
97
117
|
|
|
98
118
|
## 📋 Manual Registry
|
|
99
119
|
|
|
100
|
-
Register commands explicitly using the `@registry.register()` decorator — useful when commands
|
|
120
|
+
Register commands explicitly using the `@registry.register()` decorator — useful when you want multiple commands in a single file.
|
|
121
|
+
|
|
122
|
+
The registry style works in two ways:
|
|
123
|
+
|
|
124
|
+
**Standalone** — run the registry directly as a script:
|
|
101
125
|
|
|
102
126
|
```python
|
|
103
|
-
|
|
127
|
+
# my_commands.py
|
|
128
|
+
from python_base_command import BaseCommand, CommandError, CommandRegistry
|
|
104
129
|
|
|
105
130
|
registry = CommandRegistry()
|
|
106
131
|
|
|
@@ -108,6 +133,7 @@ registry = CommandRegistry()
|
|
|
108
133
|
@registry.register("greet")
|
|
109
134
|
class GreetCommand(BaseCommand):
|
|
110
135
|
help = "Greet a user"
|
|
136
|
+
version = "2.0.0"
|
|
111
137
|
|
|
112
138
|
def add_arguments(self, parser):
|
|
113
139
|
parser.add_argument("name", type=str)
|
|
@@ -119,6 +145,7 @@ class GreetCommand(BaseCommand):
|
|
|
119
145
|
@registry.register("export")
|
|
120
146
|
class ExportCommand(BaseCommand):
|
|
121
147
|
help = "Export data"
|
|
148
|
+
version = "3.0.0"
|
|
122
149
|
|
|
123
150
|
def add_arguments(self, parser):
|
|
124
151
|
parser.add_argument("--format", choices=["csv", "json"], default="csv")
|
|
@@ -136,10 +163,26 @@ if __name__ == "__main__":
|
|
|
136
163
|
```
|
|
137
164
|
|
|
138
165
|
```bash
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
python3 my_commands.py greet Alice
|
|
167
|
+
python3 my_commands.py export --format json
|
|
168
|
+
python3 my_commands.py export --dry-run
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Auto-discovered** — drop the registry file into your `commands/` folder and `Runner` will discover it automatically alongside any classic `Command` files:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
myapp/
|
|
175
|
+
├── cli.py
|
|
176
|
+
└── commands/
|
|
177
|
+
├── __init__.py
|
|
178
|
+
├── greet.py ← classic Command class
|
|
179
|
+
└── reg_cmd.py ← CommandRegistry with multiple commands
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
python3 cli.py --help # shows commands from both files
|
|
184
|
+
python3 cli.py greet Alice
|
|
185
|
+
python3 cli.py export --format json
|
|
143
186
|
```
|
|
144
187
|
|
|
145
188
|
---
|
|
@@ -149,7 +192,7 @@ python cli.py export --dry-run
|
|
|
149
192
|
Invoke commands programmatically — ideal for unit tests.
|
|
150
193
|
|
|
151
194
|
```python
|
|
152
|
-
from
|
|
195
|
+
from python_base_command import call_command, CommandError
|
|
153
196
|
import pytest
|
|
154
197
|
|
|
155
198
|
from commands.greet import Command as GreetCommand
|
|
@@ -180,6 +223,7 @@ Base class for all commands. Inherit from it and implement `handle()`.
|
|
|
180
223
|
| Attribute | Type | Default | Description |
|
|
181
224
|
|---|---|---|---|
|
|
182
225
|
| `help` | `str` | `""` | Description shown in `--help` |
|
|
226
|
+
| `version` | `str` | `"unknown"` | Version string exposed via `--version`. Set this per command. |
|
|
183
227
|
| `output_transaction` | `bool` | `False` | Wrap `handle()` return value in `BEGIN;` / `COMMIT;` |
|
|
184
228
|
| `suppressed_base_arguments` | `set[str]` | `set()` | Base flags to hide from `--help` |
|
|
185
229
|
| `stealth_options` | `tuple[str]` | `()` | Options used but not declared via `add_arguments()` |
|
|
@@ -191,7 +235,6 @@ Base class for all commands. Inherit from it and implement `handle()`.
|
|
|
191
235
|
|---|---|---|
|
|
192
236
|
| `handle(**kwargs)` | ✅ | Command logic. May return a string. |
|
|
193
237
|
| `add_arguments(parser)` | ❌ | Add command-specific arguments to the parser. |
|
|
194
|
-
| `get_version()` | ❌ | Override to expose your package version via `--version`. |
|
|
195
238
|
|
|
196
239
|
**`self.logger`**
|
|
197
240
|
|
|
@@ -233,7 +276,7 @@ raise CommandError("Fatal error.", returncode=2)
|
|
|
233
276
|
For commands that accept one or more arbitrary string labels. Override `handle_label()` instead of `handle()`.
|
|
234
277
|
|
|
235
278
|
```python
|
|
236
|
-
from
|
|
279
|
+
from python_base_command import LabelCommand, CommandError
|
|
237
280
|
|
|
238
281
|
|
|
239
282
|
class Command(LabelCommand):
|
|
@@ -256,24 +299,27 @@ class Command(LabelCommand):
|
|
|
256
299
|
```
|
|
257
300
|
|
|
258
301
|
```bash
|
|
259
|
-
|
|
260
|
-
|
|
302
|
+
python3 cli.py process report.csv notes.txt image.png
|
|
303
|
+
python3 cli.py process report.csv notes.txt image.png --strict
|
|
261
304
|
```
|
|
262
305
|
|
|
263
306
|
---
|
|
264
307
|
|
|
265
308
|
### `Runner`
|
|
266
309
|
|
|
267
|
-
Auto-discovers commands from a directory.
|
|
310
|
+
Auto-discovers commands from a directory. Two conventions are supported:
|
|
311
|
+
|
|
312
|
+
1. **Classic** — a `.py` file that defines a class named `Command` subclassing `BaseCommand`. The command name is the file stem.
|
|
313
|
+
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.
|
|
314
|
+
|
|
315
|
+
Files whose names start with `_` are ignored.
|
|
268
316
|
|
|
269
317
|
```python
|
|
270
|
-
from
|
|
318
|
+
from python_base_command import Runner
|
|
271
319
|
|
|
272
320
|
Runner(commands_dir="commands").run()
|
|
273
321
|
```
|
|
274
322
|
|
|
275
|
-
Files whose names start with `_` are ignored.
|
|
276
|
-
|
|
277
323
|
---
|
|
278
324
|
|
|
279
325
|
### `CommandRegistry`
|
|
@@ -281,17 +327,19 @@ Files whose names start with `_` are ignored.
|
|
|
281
327
|
Manually register commands using a decorator or programmatically.
|
|
282
328
|
|
|
283
329
|
```python
|
|
284
|
-
from
|
|
330
|
+
from python_base_command import BaseCommand, CommandRegistry
|
|
285
331
|
|
|
286
332
|
registry = CommandRegistry()
|
|
287
333
|
|
|
334
|
+
|
|
288
335
|
@registry.register("greet")
|
|
289
336
|
class GreetCommand(BaseCommand): ...
|
|
290
337
|
|
|
338
|
+
|
|
291
339
|
registry.add("export", ExportCommand) # programmatic alternative
|
|
292
340
|
|
|
293
|
-
registry.run()
|
|
294
|
-
registry.run(["myapp", "greet", "Alice"])
|
|
341
|
+
registry.run() # uses sys.argv
|
|
342
|
+
registry.run(["myapp", "greet", "Alice"]) # explicit argv
|
|
295
343
|
```
|
|
296
344
|
|
|
297
345
|
---
|
|
@@ -301,7 +349,7 @@ registry.run(["myapp", "greet", "Alice"]) # explicit argv
|
|
|
301
349
|
Invoke a command from Python code. Accepts either a class or an instance.
|
|
302
350
|
|
|
303
351
|
```python
|
|
304
|
-
from
|
|
352
|
+
from python_base_command import call_command
|
|
305
353
|
|
|
306
354
|
call_command(GreetCommand, name="Alice")
|
|
307
355
|
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.5"
|
|
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 = [
|
|
@@ -15,8 +15,6 @@ Public API
|
|
|
15
15
|
- call_command — programmatic command invocation
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
from custom_python_logger import build_logger
|
|
19
|
-
|
|
20
18
|
from .base import (
|
|
21
19
|
BaseCommand,
|
|
22
20
|
CommandError,
|
|
@@ -36,8 +34,3 @@ __all__ = [
|
|
|
36
34
|
"Runner",
|
|
37
35
|
"call_command",
|
|
38
36
|
]
|
|
39
|
-
|
|
40
|
-
build_logger(
|
|
41
|
-
project_name="python-base-command",
|
|
42
|
-
log_format="%(asctime)s | %(levelname)s | %(message)s",
|
|
43
|
-
)
|
|
@@ -6,14 +6,14 @@ 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
|
|
13
12
|
from collections.abc import Sequence
|
|
13
|
+
from importlib.metadata import version
|
|
14
14
|
from typing import Any, TextIO
|
|
15
15
|
|
|
16
|
-
from custom_python_logger import CustomLoggerAdapter, get_logger
|
|
16
|
+
from custom_python_logger import CustomLoggerAdapter, build_logger, get_logger
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"BaseCommand",
|
|
@@ -22,6 +22,7 @@ __all__ = [
|
|
|
22
22
|
"LabelCommand",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
|
+
from python_base_toolkit.utils.path_utils import get_project_path_by_file
|
|
25
26
|
|
|
26
27
|
# ---------------------------------------------------------------------------
|
|
27
28
|
# Exceptions
|
|
@@ -142,6 +143,8 @@ class BaseCommand:
|
|
|
142
143
|
----------
|
|
143
144
|
help : str
|
|
144
145
|
Short description printed in --help output.
|
|
146
|
+
version : str
|
|
147
|
+
Version string exposed via --version. Set this per command.
|
|
145
148
|
output_transaction : bool
|
|
146
149
|
If True, wrap any string returned by handle() with BEGIN; / COMMIT;.
|
|
147
150
|
suppressed_base_arguments : set[str]
|
|
@@ -153,6 +156,7 @@ class BaseCommand:
|
|
|
153
156
|
"""
|
|
154
157
|
|
|
155
158
|
help: str = ""
|
|
159
|
+
version: str = "unknown"
|
|
156
160
|
output_transaction: bool = False
|
|
157
161
|
suppressed_base_arguments: set[str] = set()
|
|
158
162
|
stealth_options: tuple[str, ...] = ()
|
|
@@ -169,22 +173,23 @@ class BaseCommand:
|
|
|
169
173
|
) -> None:
|
|
170
174
|
_ = stdout, stderr # API compatibility with call_command(stdout=..., stderr=...)
|
|
171
175
|
self.logger: CustomLoggerAdapter = get_logger(name=self.__class__.__module__.split(".", maxsplit=1)[0])
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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"
|
|
176
|
+
build_logger(
|
|
177
|
+
project_name=self.__class__.__name__,
|
|
178
|
+
log_format="%(asctime)s | %(levelname)s | %(message)s",
|
|
179
|
+
log_file=os.getenv("PYTHON_BASE_COMMAND_LOG_FILE", "true").lower() == "true",
|
|
180
|
+
)
|
|
185
181
|
|
|
186
182
|
# ------------------------------------------------------------------ parser
|
|
187
183
|
|
|
184
|
+
def set_project_version(self, project_name: str | None = None) -> None:
|
|
185
|
+
if not project_name:
|
|
186
|
+
try:
|
|
187
|
+
project_path = get_project_path_by_file()
|
|
188
|
+
project_name = project_path.name
|
|
189
|
+
except Exception:
|
|
190
|
+
self.logger.warning("Project name not provided and could not be inferred from file markers.")
|
|
191
|
+
self.version = version(project_name) if project_name else self.version
|
|
192
|
+
|
|
188
193
|
def create_parser(self, prog_name: str, subcommand: str, **kwargs: Any) -> CommandParser:
|
|
189
194
|
"""Create and return the CommandParser used to parse arguments."""
|
|
190
195
|
kwargs.setdefault("formatter_class", CommandHelpFormatter)
|
|
@@ -200,7 +205,7 @@ class BaseCommand:
|
|
|
200
205
|
parser,
|
|
201
206
|
"--version",
|
|
202
207
|
action="version",
|
|
203
|
-
version=self.
|
|
208
|
+
version=self.version,
|
|
204
209
|
help="Show program's version number and exit.",
|
|
205
210
|
)
|
|
206
211
|
self.add_base_argument(
|
|
@@ -273,7 +278,6 @@ class BaseCommand:
|
|
|
273
278
|
If handle() returns a string and output_transaction is True,
|
|
274
279
|
wraps it in BEGIN; / COMMIT;.
|
|
275
280
|
"""
|
|
276
|
-
output: str | None = None
|
|
277
281
|
if output := self.handle(**kwargs):
|
|
278
282
|
if self.output_transaction:
|
|
279
283
|
output = f"BEGIN;\n{output}\nCOMMIT;"
|
|
@@ -40,16 +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 . import
|
|
52
|
-
from .
|
|
49
|
+
from python_base_command.base import BaseCommand
|
|
50
|
+
from python_base_command.registry import CommandRegistry
|
|
53
51
|
|
|
54
52
|
logger = get_logger("python-base-command")
|
|
55
53
|
|
|
@@ -70,7 +68,7 @@ class Runner:
|
|
|
70
68
|
def __init__(
|
|
71
69
|
self,
|
|
72
70
|
commands_dir: str | Path = "commands",
|
|
73
|
-
):
|
|
71
|
+
) -> None:
|
|
74
72
|
# Resolve relative to cwd — the directory the user runs the script from,
|
|
75
73
|
# just like Django resolves manage.py commands from the project root.
|
|
76
74
|
self._commands_dir = (Path.cwd() / commands_dir).resolve()
|
|
@@ -98,8 +96,7 @@ class Runner:
|
|
|
98
96
|
if path.stem.startswith("_"):
|
|
99
97
|
continue
|
|
100
98
|
|
|
101
|
-
module
|
|
102
|
-
if module is None:
|
|
99
|
+
if (module := self._load_module(path)) is None:
|
|
103
100
|
continue
|
|
104
101
|
|
|
105
102
|
# --- 1. Classic: a top-level class named "Command" ---
|
|
@@ -112,14 +109,13 @@ class Runner:
|
|
|
112
109
|
obj = getattr(module, attr_name)
|
|
113
110
|
if isinstance(obj, CommandRegistry):
|
|
114
111
|
for name in obj.list_commands():
|
|
115
|
-
cls
|
|
116
|
-
if cls is not None:
|
|
112
|
+
if (cls := obj.get(name)) is not None:
|
|
117
113
|
commands[name] = cls
|
|
118
114
|
|
|
119
115
|
return commands
|
|
120
116
|
|
|
121
117
|
@staticmethod
|
|
122
|
-
def _load_module(path: Path) ->
|
|
118
|
+
def _load_module(path: Path) -> ModuleType | None:
|
|
123
119
|
"""Dynamically load a Python file as a module."""
|
|
124
120
|
module_name = f"_base_command_discovered_.{path.stem}"
|
|
125
121
|
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
@@ -135,7 +131,7 @@ class Runner:
|
|
|
135
131
|
|
|
136
132
|
# ------------------------------------------------------------------ running
|
|
137
133
|
|
|
138
|
-
def run(self, argv: list[str] | None = None):
|
|
134
|
+
def run(self, argv: list[str] | None = None) -> None:
|
|
139
135
|
"""
|
|
140
136
|
Parse *argv* (defaults to ``sys.argv``), discover commands, find the
|
|
141
137
|
requested one, and run it.
|
|
@@ -144,14 +140,12 @@ class Runner:
|
|
|
144
140
|
commands = self._discover()
|
|
145
141
|
|
|
146
142
|
# Show top-level help if no subcommand is given.
|
|
147
|
-
if len(argv) < 2 or argv[1] in
|
|
143
|
+
if len(argv) < 2 or argv[1] in {"-h", "--help"}:
|
|
148
144
|
self._print_help(argv[0] if argv else "unknown", commands)
|
|
149
145
|
sys.exit(0)
|
|
150
146
|
|
|
151
147
|
subcommand = argv[1]
|
|
152
|
-
command_class
|
|
153
|
-
|
|
154
|
-
if command_class is None:
|
|
148
|
+
if (command_class := commands.get(subcommand)) is None:
|
|
155
149
|
prog = argv[0] if argv else "unknown"
|
|
156
150
|
available = ", ".join(sorted(commands)) or "(none found)"
|
|
157
151
|
logger.error(
|
|
@@ -165,7 +159,7 @@ class Runner:
|
|
|
165
159
|
command_class().run_from_argv([argv[0]] + argv[2:])
|
|
166
160
|
|
|
167
161
|
@staticmethod
|
|
168
|
-
def _print_help(prog: str, commands: dict[str, type[BaseCommand]]):
|
|
162
|
+
def _print_help(prog: str, commands: dict[str, type[BaseCommand]]) -> None:
|
|
169
163
|
print(f"Usage: {prog} <command> [options]\n")
|
|
170
164
|
print("Available commands:")
|
|
171
165
|
for name, cls in sorted(commands.items()):
|
{python_base_command-0.1.3 → python_base_command-0.1.5}/usage_example/commands/registry_cmd.py
RENAMED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from python_base_command import BaseCommand, CommandParser, CommandRegistry
|
|
2
4
|
|
|
3
5
|
registry = CommandRegistry()
|
|
4
6
|
|
|
@@ -7,22 +9,28 @@ registry = CommandRegistry()
|
|
|
7
9
|
class Greet2Command(BaseCommand):
|
|
8
10
|
help = "Greet a user"
|
|
9
11
|
|
|
10
|
-
def
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
super().__init__()
|
|
14
|
+
# self.set_project_version("python-base-command")
|
|
15
|
+
self.set_project_version()
|
|
16
|
+
|
|
17
|
+
def add_arguments(self, parser: CommandParser) -> None:
|
|
11
18
|
parser.add_argument("name", type=str)
|
|
12
19
|
|
|
13
|
-
def handle(self, **kwargs):
|
|
20
|
+
def handle(self, **kwargs: Any) -> None:
|
|
14
21
|
self.logger.info(f"Hello, {kwargs['name']}!")
|
|
15
22
|
|
|
16
23
|
|
|
17
24
|
@registry.register("export")
|
|
18
25
|
class ExportCommand(BaseCommand):
|
|
19
26
|
help = "Export data"
|
|
27
|
+
version = "1.0.0"
|
|
20
28
|
|
|
21
|
-
def add_arguments(self, parser):
|
|
29
|
+
def add_arguments(self, parser: CommandParser) -> None:
|
|
22
30
|
parser.add_argument("--format", choices=["csv", "json"], default="csv")
|
|
23
31
|
parser.add_argument("--dry-run", action="store_true")
|
|
24
32
|
|
|
25
|
-
def handle(self, **kwargs):
|
|
33
|
+
def handle(self, **kwargs: Any) -> None:
|
|
26
34
|
if kwargs["dry_run"]:
|
|
27
35
|
self.logger.warning("Dry run — no files written.")
|
|
28
36
|
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_base_command-0.1.3 → python_base_command-0.1.5}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-4.1.md
RENAMED
|
File without changes
|
{python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-4o.md
RENAMED
|
File without changes
|
{python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-5-mini.md
RENAMED
|
File without changes
|
{python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-5.md
RENAMED
|
File without changes
|
|
File without changes
|
{python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/prompt.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_base_command-0.1.3 → python_base_command-0.1.5}/.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
|