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.
Files changed (42) hide show
  1. {python_base_command-0.1.3 → python_base_command-0.1.5}/PKG-INFO +76 -27
  2. {python_base_command-0.1.3 → python_base_command-0.1.5}/README.md +74 -26
  3. {python_base_command-0.1.3 → python_base_command-0.1.5}/pyproject.toml +2 -1
  4. {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/__init__.py +0 -7
  5. {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/base.py +21 -17
  6. {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/runner.py +10 -16
  7. {python_base_command-0.1.3 → python_base_command-0.1.5}/usage_example/commands/greet.py +1 -0
  8. {python_base_command-0.1.3 → python_base_command-0.1.5}/usage_example/commands/registry_cmd.py +13 -5
  9. {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/README.md +0 -0
  10. {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/black.toml +0 -0
  11. {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/pylintrc +0 -0
  12. {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/pylintrc_tests +0 -0
  13. {python_base_command-0.1.3 → python_base_command-0.1.5}/.config/ruff.toml +0 -0
  14. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/CODEOWNERS +0 -0
  15. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/chat-titles.md +0 -0
  17. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/claude-sonnet-4.md +0 -0
  18. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gemini-2.5-pro.md +0 -0
  19. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-4.1.md +0 -0
  20. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-4o.md +0 -0
  21. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-5-mini.md +0 -0
  22. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/gpt-5.md +0 -0
  23. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/nes-tab-completion.md +0 -0
  24. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/instructions/IDE Agent/prompt.md +0 -0
  25. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/pull_request_template.md +0 -0
  26. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/workflows/lint.yml +0 -0
  27. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/workflows/publish_to_pypi.yml +0 -0
  28. {python_base_command-0.1.3 → python_base_command-0.1.5}/.github/workflows/tests.yml +0 -0
  29. {python_base_command-0.1.3 → python_base_command-0.1.5}/.gitignore +0 -0
  30. {python_base_command-0.1.3 → python_base_command-0.1.5}/.pre-commit-config.yaml +0 -0
  31. {python_base_command-0.1.3 → python_base_command-0.1.5}/CHANGELOG.md +0 -0
  32. {python_base_command-0.1.3 → python_base_command-0.1.5}/LICENSE +0 -0
  33. {python_base_command-0.1.3 → python_base_command-0.1.5}/MANIFEST.in +0 -0
  34. {python_base_command-0.1.3 → python_base_command-0.1.5}/Taskfile.yml +0 -0
  35. {python_base_command-0.1.3 → python_base_command-0.1.5}/cli.py +0 -0
  36. {python_base_command-0.1.3 → python_base_command-0.1.5}/pytest.ini +0 -0
  37. {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/registry.py +0 -0
  38. {python_base_command-0.1.3 → python_base_command-0.1.5}/python_base_command/utils.py +0 -0
  39. {python_base_command-0.1.3 → python_base_command-0.1.5}/tests/__init__.py +0 -0
  40. {python_base_command-0.1.3 → python_base_command-0.1.5}/tests/test_base_command.py +0 -0
  41. {python_base_command-0.1.3 → python_base_command-0.1.5}/usage_example/__init__.py +0 -0
  42. {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
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
  ![PyPI version](https://img.shields.io/pypi/v/python-base-command)
@@ -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 base_command import Runner
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 base_command import BaseCommand, CommandError
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
- python cli.py --help # lists all available commands
112
- python cli.py greet Alice
113
- python cli.py greet Alice --shout
114
- python cli.py greet --version
115
- python cli.py greet --verbosity 2
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 live across different modules.
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
- from base_command import BaseCommand, CommandError, CommandRegistry
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
- python cli.py --help
162
- python cli.py greet Alice
163
- python cli.py export --format json
164
- python cli.py export --dry-run
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 base_command import call_command, CommandError
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 base_command import LabelCommand, CommandError
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
- python cli.py process report.csv notes.txt image.png
282
- python cli.py process report.csv notes.txt image.png --strict
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. Every `.py` file in the folder that defines a `Command` class subclassing `BaseCommand` is automatically registered.
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 base_command import Runner
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 base_command import CommandRegistry
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() # uses sys.argv
316
- registry.run(["myapp", "greet", "Alice"]) # explicit argv
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 base_command import call_command
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 base_command import Runner
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 base_command import BaseCommand, CommandError
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
- python cli.py --help # lists all available commands
90
- python cli.py greet Alice
91
- python cli.py greet Alice --shout
92
- python cli.py greet --version
93
- python cli.py greet --verbosity 2
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 live across different modules.
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
- from base_command import BaseCommand, CommandError, CommandRegistry
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
- python cli.py --help
140
- python cli.py greet Alice
141
- python cli.py export --format json
142
- python cli.py export --dry-run
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 base_command import call_command, CommandError
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 base_command import LabelCommand, CommandError
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
- python cli.py process report.csv notes.txt image.png
260
- python cli.py process report.csv notes.txt image.png --strict
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. Every `.py` file in the folder that defines a `Command` class subclassing `BaseCommand` is automatically registered.
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 base_command import Runner
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 base_command import CommandRegistry
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() # uses sys.argv
294
- registry.run(["myapp", "greet", "Alice"]) # explicit argv
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 base_command import call_command
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.3"
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
- # ------------------------------------------------------------------ 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"
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.get_version(),
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 CommandRegistry
52
- from .base import BaseCommand, CommandError
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 = self._load_module(path)
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 = obj.get(name)
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) -> Optional[ModuleType]:
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 ("-h", "--help"):
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 = commands.get(subcommand)
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()):
@@ -7,6 +7,7 @@ from python_base_command.base import CommandParser
7
7
 
8
8
  class Command(BaseCommand):
9
9
  help = "Greet a user by name"
10
+ version = "1.0.0"
10
11
 
11
12
  def add_arguments(self, parser: CommandParser) -> None:
12
13
  parser.add_argument("name", type=str, help="Name to greet")
@@ -1,4 +1,6 @@
1
- from python_base_command import BaseCommand, CommandError, CommandRegistry
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 add_arguments(self, parser):
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