webscout 7.1__py3-none-any.whl → 7.2__py3-none-any.whl

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.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (144) hide show
  1. webscout/AIauto.py +191 -191
  2. webscout/AIbase.py +122 -122
  3. webscout/AIutel.py +440 -440
  4. webscout/Bard.py +343 -161
  5. webscout/DWEBS.py +489 -492
  6. webscout/Extra/YTToolkit/YTdownloader.py +995 -995
  7. webscout/Extra/YTToolkit/__init__.py +2 -2
  8. webscout/Extra/YTToolkit/transcriber.py +476 -479
  9. webscout/Extra/YTToolkit/ytapi/channel.py +307 -307
  10. webscout/Extra/YTToolkit/ytapi/playlist.py +58 -58
  11. webscout/Extra/YTToolkit/ytapi/pool.py +7 -7
  12. webscout/Extra/YTToolkit/ytapi/utils.py +62 -62
  13. webscout/Extra/YTToolkit/ytapi/video.py +103 -103
  14. webscout/Extra/autocoder/__init__.py +9 -9
  15. webscout/Extra/autocoder/autocoder_utiles.py +199 -199
  16. webscout/Extra/autocoder/rawdog.py +5 -7
  17. webscout/Extra/autollama.py +230 -230
  18. webscout/Extra/gguf.py +3 -3
  19. webscout/Extra/weather.py +171 -171
  20. webscout/LLM.py +442 -442
  21. webscout/Litlogger/__init__.py +67 -681
  22. webscout/Litlogger/core/__init__.py +6 -0
  23. webscout/Litlogger/core/level.py +20 -0
  24. webscout/Litlogger/core/logger.py +123 -0
  25. webscout/Litlogger/handlers/__init__.py +12 -0
  26. webscout/Litlogger/handlers/console.py +50 -0
  27. webscout/Litlogger/handlers/file.py +143 -0
  28. webscout/Litlogger/handlers/network.py +174 -0
  29. webscout/Litlogger/styles/__init__.py +7 -0
  30. webscout/Litlogger/styles/colors.py +231 -0
  31. webscout/Litlogger/styles/formats.py +377 -0
  32. webscout/Litlogger/styles/text.py +87 -0
  33. webscout/Litlogger/utils/__init__.py +6 -0
  34. webscout/Litlogger/utils/detectors.py +154 -0
  35. webscout/Litlogger/utils/formatters.py +200 -0
  36. webscout/Provider/AISEARCH/DeepFind.py +250 -250
  37. webscout/Provider/Blackboxai.py +3 -3
  38. webscout/Provider/ChatGPTGratis.py +226 -0
  39. webscout/Provider/Cloudflare.py +3 -4
  40. webscout/Provider/DeepSeek.py +218 -0
  41. webscout/Provider/Deepinfra.py +3 -3
  42. webscout/Provider/Free2GPT.py +131 -124
  43. webscout/Provider/Gemini.py +100 -115
  44. webscout/Provider/Glider.py +3 -3
  45. webscout/Provider/Groq.py +5 -1
  46. webscout/Provider/Jadve.py +3 -3
  47. webscout/Provider/Marcus.py +191 -192
  48. webscout/Provider/Netwrck.py +3 -3
  49. webscout/Provider/PI.py +2 -2
  50. webscout/Provider/PizzaGPT.py +2 -3
  51. webscout/Provider/QwenLM.py +311 -0
  52. webscout/Provider/TTI/AiForce/__init__.py +22 -22
  53. webscout/Provider/TTI/AiForce/async_aiforce.py +257 -257
  54. webscout/Provider/TTI/AiForce/sync_aiforce.py +242 -242
  55. webscout/Provider/TTI/Nexra/__init__.py +22 -22
  56. webscout/Provider/TTI/Nexra/async_nexra.py +286 -286
  57. webscout/Provider/TTI/Nexra/sync_nexra.py +258 -258
  58. webscout/Provider/TTI/PollinationsAI/__init__.py +23 -23
  59. webscout/Provider/TTI/PollinationsAI/async_pollinations.py +330 -330
  60. webscout/Provider/TTI/PollinationsAI/sync_pollinations.py +285 -285
  61. webscout/Provider/TTI/artbit/__init__.py +22 -22
  62. webscout/Provider/TTI/artbit/async_artbit.py +184 -184
  63. webscout/Provider/TTI/artbit/sync_artbit.py +176 -176
  64. webscout/Provider/TTI/blackbox/__init__.py +4 -4
  65. webscout/Provider/TTI/blackbox/async_blackbox.py +212 -212
  66. webscout/Provider/TTI/blackbox/sync_blackbox.py +199 -199
  67. webscout/Provider/TTI/deepinfra/__init__.py +4 -4
  68. webscout/Provider/TTI/deepinfra/async_deepinfra.py +227 -227
  69. webscout/Provider/TTI/deepinfra/sync_deepinfra.py +199 -199
  70. webscout/Provider/TTI/huggingface/__init__.py +22 -22
  71. webscout/Provider/TTI/huggingface/async_huggingface.py +199 -199
  72. webscout/Provider/TTI/huggingface/sync_huggingface.py +195 -195
  73. webscout/Provider/TTI/imgninza/__init__.py +4 -4
  74. webscout/Provider/TTI/imgninza/async_ninza.py +214 -214
  75. webscout/Provider/TTI/imgninza/sync_ninza.py +209 -209
  76. webscout/Provider/TTI/talkai/__init__.py +4 -4
  77. webscout/Provider/TTI/talkai/async_talkai.py +229 -229
  78. webscout/Provider/TTI/talkai/sync_talkai.py +207 -207
  79. webscout/Provider/TTS/deepgram.py +182 -182
  80. webscout/Provider/TTS/elevenlabs.py +136 -136
  81. webscout/Provider/TTS/gesserit.py +150 -150
  82. webscout/Provider/TTS/murfai.py +138 -138
  83. webscout/Provider/TTS/parler.py +133 -134
  84. webscout/Provider/TTS/streamElements.py +360 -360
  85. webscout/Provider/TTS/utils.py +280 -280
  86. webscout/Provider/TTS/voicepod.py +116 -116
  87. webscout/Provider/TextPollinationsAI.py +2 -3
  88. webscout/Provider/WiseCat.py +193 -0
  89. webscout/Provider/__init__.py +144 -134
  90. webscout/Provider/cerebras.py +242 -227
  91. webscout/Provider/chatglm.py +204 -204
  92. webscout/Provider/dgaf.py +2 -3
  93. webscout/Provider/gaurish.py +2 -3
  94. webscout/Provider/geminiapi.py +208 -208
  95. webscout/Provider/granite.py +223 -0
  96. webscout/Provider/hermes.py +218 -218
  97. webscout/Provider/llama3mitril.py +179 -179
  98. webscout/Provider/llamatutor.py +3 -3
  99. webscout/Provider/llmchat.py +2 -3
  100. webscout/Provider/meta.py +794 -794
  101. webscout/Provider/multichat.py +331 -331
  102. webscout/Provider/typegpt.py +359 -359
  103. webscout/Provider/yep.py +2 -2
  104. webscout/__main__.py +5 -5
  105. webscout/cli.py +319 -319
  106. webscout/conversation.py +241 -242
  107. webscout/exceptions.py +328 -328
  108. webscout/litagent/__init__.py +28 -28
  109. webscout/litagent/agent.py +2 -3
  110. webscout/litprinter/__init__.py +0 -58
  111. webscout/scout/__init__.py +8 -8
  112. webscout/scout/core.py +884 -884
  113. webscout/scout/element.py +459 -459
  114. webscout/scout/parsers/__init__.py +69 -69
  115. webscout/scout/parsers/html5lib_parser.py +172 -172
  116. webscout/scout/parsers/html_parser.py +236 -236
  117. webscout/scout/parsers/lxml_parser.py +178 -178
  118. webscout/scout/utils.py +38 -38
  119. webscout/swiftcli/__init__.py +811 -811
  120. webscout/update_checker.py +2 -12
  121. webscout/version.py +1 -1
  122. webscout/webscout_search.py +5 -4
  123. webscout/zeroart/__init__.py +54 -54
  124. webscout/zeroart/base.py +60 -60
  125. webscout/zeroart/effects.py +99 -99
  126. webscout/zeroart/fonts.py +816 -816
  127. {webscout-7.1.dist-info → webscout-7.2.dist-info}/METADATA +4 -3
  128. webscout-7.2.dist-info/RECORD +217 -0
  129. webstoken/__init__.py +30 -30
  130. webstoken/classifier.py +189 -189
  131. webstoken/keywords.py +216 -216
  132. webstoken/language.py +128 -128
  133. webstoken/ner.py +164 -164
  134. webstoken/normalizer.py +35 -35
  135. webstoken/processor.py +77 -77
  136. webstoken/sentiment.py +206 -206
  137. webstoken/stemmer.py +73 -73
  138. webstoken/tagger.py +60 -60
  139. webstoken/tokenizer.py +158 -158
  140. webscout-7.1.dist-info/RECORD +0 -198
  141. {webscout-7.1.dist-info → webscout-7.2.dist-info}/LICENSE.md +0 -0
  142. {webscout-7.1.dist-info → webscout-7.2.dist-info}/WHEEL +0 -0
  143. {webscout-7.1.dist-info → webscout-7.2.dist-info}/entry_points.txt +0 -0
  144. {webscout-7.1.dist-info → webscout-7.2.dist-info}/top_level.txt +0 -0
@@ -1,811 +1,811 @@
1
- """
2
- SwiftCLI - A powerful Python CLI framework
3
-
4
- A modern, feature-rich CLI framework for building awesome command-line applications.
5
- Built with love for the Python community!
6
-
7
- Basic Usage:
8
- >>> from swiftcli import CLI
9
- >>> app = CLI(name="my-app", help="My awesome CLI app")
10
- >>> @app.command()
11
- ... def hello(name: str):
12
- ... '''Say hello to someone'''
13
- ... print(f"Hello {name}!")
14
- >>> app.run()
15
-
16
- Advanced Usage:
17
- >>> @app.group()
18
- ... def db():
19
- ... '''Database commands'''
20
- ... pass
21
- >>>
22
- >>> @db.command()
23
- ... def migrate():
24
- ... '''Run database migrations'''
25
- ... print("Running migrations...")
26
-
27
- For more examples, check out the documentation!
28
- """
29
-
30
- import importlib
31
- import os
32
- import sys
33
- import json
34
- import inspect
35
- from typing import Any, Dict, List, Optional, Type, Union, Callable
36
- from functools import wraps
37
- from pathlib import Path
38
- from rich.console import Console
39
- from rich.table import Table
40
- from rich.progress import Progress, SpinnerColumn, TextColumn
41
- from rich.theme import Theme
42
-
43
- # Console setup
44
- console = Console()
45
-
46
- class UsageError(Exception):
47
- """Raised when CLI is used incorrectly"""
48
- pass
49
-
50
- class BadParameter(UsageError):
51
- """Raised when a parameter is invalid"""
52
- pass
53
-
54
- class Context:
55
- """
56
- Context object that holds state for the CLI app.
57
-
58
- Attributes:
59
- cli (CLI): The CLI application instance.
60
- parent (Context): The parent context.
61
- command (str): The current command.
62
- obj (Any): The current object.
63
- params (Dict[str, Any]): The current parameters.
64
- """
65
- def __init__(
66
- self,
67
- cli: 'CLI',
68
- parent: Optional['Context'] = None,
69
- command: Optional[str] = None,
70
- obj: Any = None
71
- ):
72
- self.cli = cli
73
- self.parent = parent
74
- self.command = command
75
- self.obj = obj
76
- self.params = {}
77
-
78
- class Plugin:
79
- """
80
- Base class for SwiftCLI plugins.
81
-
82
- Attributes:
83
- app (CLI): The CLI application instance.
84
- enabled (bool): Whether the plugin is enabled.
85
- config (Dict[str, Any]): The plugin configuration.
86
- """
87
- def __init__(self):
88
- self.app = None
89
- self.enabled = True
90
- self.config: Dict[str, Any] = {}
91
-
92
- def init_app(self, app):
93
- """Initialize plugin with CLI app instance"""
94
- self.app = app
95
-
96
- def before_command(self, command: str, args: List[str]) -> Optional[bool]:
97
- """Called before command execution"""
98
- pass
99
-
100
- def after_command(self, command: str, args: List[str], result: Any):
101
- """Called after command execution"""
102
- pass
103
-
104
- def on_error(self, command: str, error: Exception):
105
- """Called when command raises an error"""
106
- pass
107
-
108
- def on_help(self, command: str) -> Optional[str]:
109
- """Called when help is requested for a command"""
110
- pass
111
-
112
- def on_completion(self, command: str, incomplete: str) -> List[str]:
113
- """Called when shell completion is requested"""
114
- return []
115
-
116
- class PluginManager:
117
- """
118
- Manages SwiftCLI plugins.
119
-
120
- Attributes:
121
- plugins (List[Plugin]): The list of plugins.
122
- plugin_dir (str): The plugin directory.
123
- """
124
- def __init__(self):
125
- self.plugins: List[Plugin] = []
126
- self.plugin_dir = os.path.expanduser("~/.swiftcli/plugins")
127
- os.makedirs(self.plugin_dir, exist_ok=True)
128
- sys.path.append(self.plugin_dir)
129
-
130
- def register(self, plugin: Plugin):
131
- """Register a new plugin"""
132
- self.plugins.append(plugin)
133
-
134
- def load_plugins(self):
135
- """Load all plugins from plugin directory"""
136
- for file in Path(self.plugin_dir).glob("*.py"):
137
- if file.name.startswith("_"):
138
- continue
139
- try:
140
- module = importlib.import_module(file.stem)
141
- for attr_name in dir(module):
142
- attr = getattr(module, attr_name)
143
- if (isinstance(attr, type) and
144
- issubclass(attr, Plugin) and
145
- attr is not Plugin):
146
- plugin = attr()
147
- self.register(plugin)
148
- except Exception as e:
149
- console.print(f"[red]Error loading plugin {file.name}: {e}[/red]")
150
-
151
- def before_command(self, command: str, args: List[str]) -> bool:
152
- """Run before_command hooks"""
153
- for plugin in self.plugins:
154
- if not plugin.enabled:
155
- continue
156
- try:
157
- result = plugin.before_command(command, args)
158
- if result is False:
159
- return False
160
- except Exception as e:
161
- console.print(f"[red]Error in plugin {plugin.__class__.__name__}: {e}[/red]")
162
- return True
163
-
164
- def after_command(self, command: str, args: List[str], result: Any):
165
- """Run after_command hooks"""
166
- for plugin in self.plugins:
167
- if not plugin.enabled:
168
- continue
169
- try:
170
- plugin.after_command(command, args, result)
171
- except Exception as e:
172
- console.print(f"[red]Error in plugin {plugin.__class__.__name__}: {e}[/red]")
173
-
174
- def on_error(self, command: str, error: Exception):
175
- """Run error hooks"""
176
- for plugin in self.plugins:
177
- if not plugin.enabled:
178
- continue
179
- try:
180
- plugin.on_error(command, error)
181
- except Exception as e:
182
- console.print(f"[red]Error in plugin {plugin.__class__.__name__}: {e}[/red]")
183
-
184
- class Group:
185
- """
186
- Command group that can contain subcommands and be chained.
187
-
188
- Basic Usage:
189
- >>> @app.group()
190
- ... def db():
191
- ... '''Database commands'''
192
- ... pass
193
- >>> @db.command()
194
- ... def migrate():
195
- ... '''Run migrations'''
196
- ... pass
197
-
198
- Advanced Usage:
199
- >>> @app.group(chain=True)
200
- ... def process():
201
- ... '''Process data'''
202
- ... pass
203
- >>> @process.command()
204
- ... def validate():
205
- ... '''Validate data'''
206
- ... pass
207
- """
208
- def __init__(
209
- self,
210
- name: str = None,
211
- help: str = None,
212
- chain: bool = False,
213
- invoke_without_command: bool = False
214
- ):
215
- self.name = name
216
- self.help = help
217
- self.chain = chain
218
- self.invoke_without_command = invoke_without_command
219
- self.commands = {}
220
-
221
- def command(
222
- self,
223
- name: str = None,
224
- help: str = None,
225
- aliases: List[str] = None,
226
- hidden: bool = False
227
- ):
228
- """Register a new command"""
229
- def decorator(f):
230
- cmd_name = name or f.__name__
231
- self.commands[cmd_name] = {
232
- 'func': f,
233
- 'help': help or f.__doc__,
234
- 'aliases': aliases or [],
235
- 'hidden': hidden
236
- }
237
- return f
238
- return decorator
239
-
240
- def group(self, *args, **kwargs):
241
- """Create a subgroup"""
242
- def decorator(f):
243
- subgroup = Group(*args, **kwargs)
244
- self.commands[subgroup.name] = subgroup
245
- return subgroup
246
- return decorator
247
-
248
- def run(self, args: List[str]):
249
- """Run the group command"""
250
- if not args or args[0] in ['-h', '--help']:
251
- self._print_help()
252
- return
253
-
254
- command_name = args[0]
255
- command_args = args[1:]
256
-
257
- if command_name not in self.commands:
258
- console.print(f"[red]Unknown command: {command_name}[/red]")
259
- self._print_help()
260
- return 1
261
-
262
- command = self.commands[command_name]
263
- try:
264
- result = command['func'](**self._parse_args(command, command_args))
265
- if self.chain and result is not None:
266
- return result
267
- except Exception as e:
268
- console.print(f"[red]Error: {str(e)}[/red]")
269
- return 1
270
-
271
- def _parse_args(self, command: Dict, args: List[str]) -> Dict[str, Any]:
272
- """Parse command arguments"""
273
- params = {}
274
- func = command['func']
275
- sig = inspect.signature(func)
276
-
277
- # Handle options
278
- if hasattr(func, '_options'):
279
- for opt in func._options:
280
- # Get the destination parameter name from the longest option
281
- param_decls = sorted(opt['param_decls'], key=len, reverse=True)
282
- param_name = param_decls[0].lstrip('-').replace('-', '_')
283
-
284
- # If there's a parameter name in the signature, use that instead
285
- for param in sig.parameters.values():
286
- if param.name in [d.lstrip('-').replace('-', '_') for d in param_decls]:
287
- param_name = param.name
288
- break
289
-
290
- found = False
291
- multiple_values = []
292
-
293
- # Check for long and short options
294
- i = 0
295
- while i < len(args):
296
- if args[i] in opt['param_decls']:
297
- if opt.get('is_flag', False):
298
- if opt.get('multiple', False):
299
- multiple_values.append(True)
300
- else:
301
- params[param_name] = True
302
- else:
303
- if i + 1 < len(args):
304
- value = args[i + 1]
305
- # Convert value to the correct type
306
- if 'type' in opt:
307
- try:
308
- value = opt['type'](value)
309
- except ValueError:
310
- raise UsageError(f"Invalid value for {args[i]}: {value}")
311
-
312
- if opt.get('multiple', False):
313
- multiple_values.append(value)
314
- else:
315
- params[param_name] = value
316
- args.pop(i + 1)
317
- else:
318
- raise UsageError(f"Option {args[i]} requires a value")
319
- args.pop(i)
320
- found = True
321
- if not opt.get('multiple', False):
322
- break
323
- else:
324
- i += 1
325
-
326
- # Set multiple values if any
327
- if multiple_values:
328
- params[param_name] = multiple_values
329
-
330
- # Handle required options
331
- if not found and opt.get('required', False):
332
- raise UsageError(f"Option {opt['param_decls'][0]} is required")
333
-
334
- # Set default value if not found
335
- if not found and 'default' in opt:
336
- params[param_name] = opt['default']
337
-
338
- # Handle arguments
339
- if hasattr(func, '_arguments'):
340
- for i, arg in enumerate(func._arguments):
341
- if i < len(args):
342
- value = args[i]
343
- # Convert value to the correct type
344
- if 'type' in arg:
345
- try:
346
- value = arg['type'](value)
347
- except ValueError:
348
- raise UsageError(f"Invalid value for {arg['name']}: {value}")
349
- params[arg['name']] = value
350
- elif arg.get('required', True):
351
- raise UsageError(f"Argument {arg['name']} is required")
352
- elif 'default' in arg:
353
- params[arg['name']] = arg['default']
354
-
355
- # Handle environment variables
356
- if hasattr(func, '_envvars'):
357
- for env in func._envvars:
358
- value = os.environ.get(env['name'])
359
- if env.get('required', False) and not value:
360
- raise UsageError(f"Environment variable {env['name']} is required")
361
- if value:
362
- # Convert value to the correct type
363
- if 'type' in env:
364
- try:
365
- value = env['type'](value)
366
- except ValueError:
367
- raise UsageError(f"Invalid value for {env['name']}: {value}")
368
- params[env['name'].lower()] = value
369
-
370
- return params
371
-
372
- def _print_help(self):
373
- """Print group help message"""
374
- console.print(f"\n{self.name} commands:")
375
- if self.help:
376
- console.print(f"\n{self.help}")
377
-
378
- for name, cmd in self.commands.items():
379
- if not cmd.get('hidden', False):
380
- console.print(f" {name:20} {cmd['help'] or ''}")
381
-
382
- console.print("\nUse -h or --help with any command for more info")
383
-
384
- class CLI:
385
- """
386
- The main CLI application class that handles all command registration and execution.
387
-
388
- Basic Usage:
389
- >>> from swiftcli import CLI
390
- >>> app = CLI("myapp")
391
- >>> @app.command()
392
- ... def greet(name: str):
393
- ... print(f"Hello {name}!")
394
- >>> app.run()
395
-
396
- Advanced Usage:
397
- >>> app = CLI("myapp", version="1.0.0")
398
- >>> @app.group()
399
- ... def config():
400
- ... '''Manage configuration'''
401
- ... pass
402
- >>> @config.command()
403
- ... def set(key: str, value: str):
404
- ... '''Set config value'''
405
- ... print(f"Setting {key}={value}")
406
- """
407
- def __init__(
408
- self,
409
- name: str = None,
410
- help: str = None,
411
- version: str = None,
412
- chain: bool = False
413
- ):
414
- self.name = name
415
- self.help = help
416
- self.version = version
417
- self.chain = chain
418
- self.commands = {}
419
- self.groups = {}
420
- self.plugin_manager = PluginManager()
421
-
422
- def command(
423
- self,
424
- name: str = None,
425
- help: str = None,
426
- aliases: List[str] = None,
427
- hidden: bool = False
428
- ):
429
- """
430
- Decorator to register a new command.
431
-
432
- Basic Usage:
433
- >>> @app.command()
434
- ... def hello(name: str):
435
- ... '''Say hello'''
436
- ... print(f"Hello {name}!")
437
-
438
- Advanced Usage:
439
- >>> @app.command(name="greet", aliases=["hi", "hey"])
440
- ... def hello(name: str):
441
- ... '''Greet someone'''
442
- ... print(f"Hello {name}!")
443
- """
444
- def decorator(f):
445
- cmd_name = name or f.__name__
446
- self.commands[cmd_name] = {
447
- 'func': f,
448
- 'help': help or f.__doc__,
449
- 'aliases': aliases or [],
450
- 'hidden': hidden
451
- }
452
- return f
453
- return decorator
454
-
455
- def group(
456
- self,
457
- name: str = None,
458
- help: str = None,
459
- chain: bool = False,
460
- **kwargs
461
- ):
462
- """Create a command group"""
463
- def decorator(f):
464
- if hasattr(f, '_group'):
465
- group_info = f._group
466
- group = Group(
467
- name=group_info['name'],
468
- help=group_info['help'],
469
- chain=group_info['chain'],
470
- invoke_without_command=group_info['invoke_without_command']
471
- )
472
- else:
473
- group = Group(
474
- name=name or f.__name__,
475
- help=help or f.__doc__,
476
- chain=chain
477
- )
478
- self.groups[group.name] = group
479
- return group
480
- return decorator
481
-
482
- def run(self, args: List[str] = None):
483
- """Run the CLI application"""
484
- args = args or sys.argv[1:]
485
-
486
- if not args or args[0] in ['-h', '--help']:
487
- self._print_help()
488
- return
489
-
490
- if args[0] in ['-v', '--version'] and self.version:
491
- console.print(self.version)
492
- return
493
-
494
- command_name = args[0]
495
- command_args = args[1:]
496
-
497
- # Check if it's a group command
498
- if command_name in self.groups:
499
- group = self.groups[command_name]
500
- if len(command_args) == 0:
501
- if not group.invoke_without_command:
502
- group._print_help()
503
- return
504
- else:
505
- return group.run(command_args)
506
-
507
- # Regular command
508
- if command_name not in self.commands:
509
- console.print(f"[red]Unknown command: {command_name}[/red]")
510
- self._print_help()
511
- return 1
512
-
513
- command = self.commands[command_name]
514
- try:
515
- ctx = Context(self, command=command_name)
516
- result = command['func'](**self._parse_args(command, command_args))
517
-
518
- if self.chain and result is not None:
519
- return result
520
-
521
- except Exception as e:
522
- console.print(f"[red]Error: {str(e)}[/red]")
523
- return 1
524
-
525
- def _parse_args(self, command: Dict, args: List[str]) -> Dict[str, Any]:
526
- """Parse command arguments"""
527
- params = {}
528
- func = command['func']
529
- sig = inspect.signature(func)
530
-
531
- # Handle options
532
- if hasattr(func, '_options'):
533
- for opt in func._options:
534
- # Get the destination parameter name from the longest option
535
- param_decls = sorted(opt['param_decls'], key=len, reverse=True)
536
- param_name = param_decls[0].lstrip('-').replace('-', '_')
537
-
538
- # If there's a parameter name in the signature, use that instead
539
- for param in sig.parameters.values():
540
- if param.name in [d.lstrip('-').replace('-', '_') for d in param_decls]:
541
- param_name = param.name
542
- break
543
-
544
- found = False
545
- multiple_values = []
546
-
547
- # Check for long and short options
548
- i = 0
549
- while i < len(args):
550
- if args[i] in opt['param_decls']:
551
- if opt.get('is_flag', False):
552
- if opt.get('multiple', False):
553
- multiple_values.append(True)
554
- else:
555
- params[param_name] = True
556
- else:
557
- if i + 1 < len(args):
558
- value = args[i + 1]
559
- # Convert value to the correct type
560
- if 'type' in opt:
561
- try:
562
- value = opt['type'](value)
563
- except ValueError:
564
- raise UsageError(f"Invalid value for {args[i]}: {value}")
565
-
566
- if opt.get('multiple', False):
567
- multiple_values.append(value)
568
- else:
569
- params[param_name] = value
570
- args.pop(i + 1)
571
- else:
572
- raise UsageError(f"Option {args[i]} requires a value")
573
- args.pop(i)
574
- found = True
575
- if not opt.get('multiple', False):
576
- break
577
- else:
578
- i += 1
579
-
580
- # Set multiple values if any
581
- if multiple_values:
582
- params[param_name] = multiple_values
583
-
584
- # Handle required options
585
- if not found and opt.get('required', False):
586
- raise UsageError(f"Option {opt['param_decls'][0]} is required")
587
-
588
- # Set default value if not found
589
- if not found and 'default' in opt:
590
- params[param_name] = opt['default']
591
-
592
- # Handle arguments
593
- if hasattr(func, '_arguments'):
594
- for i, arg in enumerate(func._arguments):
595
- if i < len(args):
596
- value = args[i]
597
- # Convert value to the correct type
598
- if 'type' in arg:
599
- try:
600
- value = arg['type'](value)
601
- except ValueError:
602
- raise UsageError(f"Invalid value for {arg['name']}: {value}")
603
- params[arg['name']] = value
604
- elif arg.get('required', True):
605
- raise UsageError(f"Argument {arg['name']} is required")
606
- elif 'default' in arg:
607
- params[arg['name']] = arg['default']
608
-
609
- # Handle environment variables
610
- if hasattr(func, '_envvars'):
611
- for env in func._envvars:
612
- value = os.environ.get(env['name'])
613
- if env.get('required', False) and not value:
614
- raise UsageError(f"Environment variable {env['name']} is required")
615
- if value:
616
- # Convert value to the correct type
617
- if 'type' in env:
618
- try:
619
- value = env['type'](value)
620
- except ValueError:
621
- raise UsageError(f"Invalid value for {env['name']}: {value}")
622
- params[env['name'].lower()] = value
623
-
624
- return params
625
-
626
- def _print_help(self):
627
- """Print main help message"""
628
- console.print(f"\n{self.name or 'CLI Application'}")
629
- if self.help:
630
- console.print(f"\n{self.help}")
631
-
632
- console.print("\nCommands:")
633
- for name, cmd in self.commands.items():
634
- if not cmd.get('hidden', False):
635
- console.print(f" {name:20} {cmd['help'] or ''}")
636
-
637
- for name, group in self.groups.items():
638
- console.print(f"\n{name} commands:")
639
- for cmd_name, cmd in group.commands.items():
640
- if not cmd.get('hidden', False):
641
- console.print(f" {name} {cmd_name:20} {cmd['help'] or ''}")
642
-
643
- console.print("\nUse -h or --help with any command for more info")
644
-
645
- def command(
646
- name: str = None,
647
- help: str = None,
648
- aliases: List[str] = None,
649
- hidden: bool = False
650
- ):
651
- """
652
- Decorator to register a new command.
653
-
654
- Basic Usage:
655
- >>> @app.command()
656
- ... def hello(name: str):
657
- ... '''Say hello'''
658
- ... print(f"Hello {name}!")
659
-
660
- Advanced Usage:
661
- >>> @app.command(name="greet", aliases=["hi", "hey"])
662
- ... def hello(name: str):
663
- ... '''Greet someone'''
664
- ... print(f"Hello {name}!")
665
- """
666
- def decorator(f: Callable) -> Callable:
667
- f._command = {
668
- 'name': name or f.__name__,
669
- 'help': help or f.__doc__,
670
- 'aliases': aliases or [],
671
- 'hidden': hidden
672
- }
673
- return f
674
- return decorator
675
-
676
- def option(*param_decls, **attrs):
677
- """
678
- Decorator to add an option to a command.
679
-
680
- Basic Usage:
681
- >>> @app.command()
682
- ... @option("--count", type=int, default=1)
683
- ... def repeat(count: int, message: str):
684
- ... '''Repeat a message'''
685
- ... for _ in range(count):
686
- ... print(message)
687
-
688
- Advanced Usage:
689
- >>> @app.command()
690
- ... @option("--format", "-f", type=click.Choice(["json", "yaml"]))
691
- ... def export(format: str):
692
- ... '''Export data'''
693
- ... print(f"Exporting as {format}")
694
- """
695
- def decorator(f: Callable) -> Callable:
696
- if not hasattr(f, '_options'):
697
- f._options = []
698
-
699
- # Set default values
700
- attrs.setdefault('type', str)
701
- attrs.setdefault('required', False)
702
- attrs.setdefault('default', None)
703
- attrs.setdefault('help', None)
704
- attrs.setdefault('is_flag', False)
705
- attrs.setdefault('multiple', False)
706
- attrs.setdefault('count', False)
707
- attrs.setdefault('prompt', False)
708
- attrs.setdefault('hide_input', False)
709
- attrs.setdefault('confirmation_prompt', False)
710
- attrs.setdefault('choices', None)
711
- attrs.setdefault('callback', None)
712
- attrs.setdefault('show_default', True)
713
- attrs.setdefault('hidden', False)
714
-
715
- f._options.append({
716
- 'param_decls': param_decls,
717
- **attrs
718
- })
719
- return f
720
- return decorator
721
-
722
- def argument(name: str, **attrs):
723
- """Argument decorator"""
724
- def decorator(f: Callable) -> Callable:
725
- if not hasattr(f, '_arguments'):
726
- f._arguments = []
727
- f._arguments.append({
728
- 'name': name,
729
- **attrs
730
- })
731
- return f
732
- return decorator
733
-
734
- def group(
735
- name: str = None,
736
- help: str = None,
737
- chain: bool = False,
738
- invoke_without_command: bool = False
739
- ):
740
- """Group decorator"""
741
- def decorator(f: Callable) -> Callable:
742
- f._group = {
743
- 'name': name or f.__name__,
744
- 'help': help or f.__doc__,
745
- 'chain': chain,
746
- 'invoke_without_command': invoke_without_command
747
- }
748
- return f
749
- return decorator
750
-
751
- def pass_context(f: Callable) -> Callable:
752
- """Pass context decorator"""
753
- f._pass_context = True
754
- return f
755
-
756
- def envvar(name: str, help: str = None, required: bool = False):
757
- """Environment variable decorator"""
758
- def decorator(f: Callable) -> Callable:
759
- if not hasattr(f, '_envvars'):
760
- f._envvars = []
761
- f._envvars.append({
762
- 'name': name,
763
- 'help': help,
764
- 'required': required
765
- })
766
- return f
767
- return decorator
768
-
769
- def config_file(path: str = None, auto_create: bool = True):
770
- """Configuration file decorator"""
771
- def decorator(f: Callable) -> Callable:
772
- f._config = {
773
- 'path': path,
774
- 'auto_create': auto_create
775
- }
776
- return f
777
- return decorator
778
-
779
- def table_output(headers: List[str], style: str = None):
780
- """Table output decorator"""
781
- def decorator(f: Callable) -> Callable:
782
- @wraps(f)
783
- def wrapper(*args, **kwargs):
784
- result = f(*args, **kwargs)
785
- if result:
786
- table = Table(show_header=True, header_style="bold blue")
787
- for header in headers:
788
- table.add_column(header)
789
- for row in result:
790
- table.add_row(*[str(cell) for cell in row])
791
- console.print(table)
792
- return result
793
- return wrapper
794
- return decorator
795
-
796
- def progress(description: str = None):
797
- """Progress decorator"""
798
- def decorator(f: Callable) -> Callable:
799
- @wraps(f)
800
- def wrapper(*args, **kwargs):
801
- with Progress(
802
- SpinnerColumn(),
803
- TextColumn("[progress.description]{task.description}"),
804
- transient=True,
805
- ) as progress:
806
- task = progress.add_task(description or f.__name__, total=None)
807
- result = f(*args, **kwargs)
808
- progress.update(task, completed=True)
809
- return result
810
- return wrapper
811
- return decorator
1
+ """
2
+ SwiftCLI - A powerful Python CLI framework
3
+
4
+ A modern, feature-rich CLI framework for building awesome command-line applications.
5
+ Built with love for the Python community!
6
+
7
+ Basic Usage:
8
+ >>> from swiftcli import CLI
9
+ >>> app = CLI(name="my-app", help="My awesome CLI app")
10
+ >>> @app.command()
11
+ ... def hello(name: str):
12
+ ... '''Say hello to someone'''
13
+ ... print(f"Hello {name}!")
14
+ >>> app.run()
15
+
16
+ Advanced Usage:
17
+ >>> @app.group()
18
+ ... def db():
19
+ ... '''Database commands'''
20
+ ... pass
21
+ >>>
22
+ >>> @db.command()
23
+ ... def migrate():
24
+ ... '''Run database migrations'''
25
+ ... print("Running migrations...")
26
+
27
+ For more examples, check out the documentation!
28
+ """
29
+
30
+ import importlib
31
+ import os
32
+ import sys
33
+ import json
34
+ import inspect
35
+ from typing import Any, Dict, List, Optional, Type, Union, Callable
36
+ from functools import wraps
37
+ from pathlib import Path
38
+ from rich.console import Console
39
+ from rich.table import Table
40
+ from rich.progress import Progress, SpinnerColumn, TextColumn
41
+ from rich.theme import Theme
42
+
43
+ # Console setup
44
+ console = Console()
45
+
46
+ class UsageError(Exception):
47
+ """Raised when CLI is used incorrectly"""
48
+ pass
49
+
50
+ class BadParameter(UsageError):
51
+ """Raised when a parameter is invalid"""
52
+ pass
53
+
54
+ class Context:
55
+ """
56
+ Context object that holds state for the CLI app.
57
+
58
+ Attributes:
59
+ cli (CLI): The CLI application instance.
60
+ parent (Context): The parent context.
61
+ command (str): The current command.
62
+ obj (Any): The current object.
63
+ params (Dict[str, Any]): The current parameters.
64
+ """
65
+ def __init__(
66
+ self,
67
+ cli: 'CLI',
68
+ parent: Optional['Context'] = None,
69
+ command: Optional[str] = None,
70
+ obj: Any = None
71
+ ):
72
+ self.cli = cli
73
+ self.parent = parent
74
+ self.command = command
75
+ self.obj = obj
76
+ self.params = {}
77
+
78
+ class Plugin:
79
+ """
80
+ Base class for SwiftCLI plugins.
81
+
82
+ Attributes:
83
+ app (CLI): The CLI application instance.
84
+ enabled (bool): Whether the plugin is enabled.
85
+ config (Dict[str, Any]): The plugin configuration.
86
+ """
87
+ def __init__(self):
88
+ self.app = None
89
+ self.enabled = True
90
+ self.config: Dict[str, Any] = {}
91
+
92
+ def init_app(self, app):
93
+ """Initialize plugin with CLI app instance"""
94
+ self.app = app
95
+
96
+ def before_command(self, command: str, args: List[str]) -> Optional[bool]:
97
+ """Called before command execution"""
98
+ pass
99
+
100
+ def after_command(self, command: str, args: List[str], result: Any):
101
+ """Called after command execution"""
102
+ pass
103
+
104
+ def on_error(self, command: str, error: Exception):
105
+ """Called when command raises an error"""
106
+ pass
107
+
108
+ def on_help(self, command: str) -> Optional[str]:
109
+ """Called when help is requested for a command"""
110
+ pass
111
+
112
+ def on_completion(self, command: str, incomplete: str) -> List[str]:
113
+ """Called when shell completion is requested"""
114
+ return []
115
+
116
+ class PluginManager:
117
+ """
118
+ Manages SwiftCLI plugins.
119
+
120
+ Attributes:
121
+ plugins (List[Plugin]): The list of plugins.
122
+ plugin_dir (str): The plugin directory.
123
+ """
124
+ def __init__(self):
125
+ self.plugins: List[Plugin] = []
126
+ self.plugin_dir = os.path.expanduser("~/.swiftcli/plugins")
127
+ os.makedirs(self.plugin_dir, exist_ok=True)
128
+ sys.path.append(self.plugin_dir)
129
+
130
+ def register(self, plugin: Plugin):
131
+ """Register a new plugin"""
132
+ self.plugins.append(plugin)
133
+
134
+ def load_plugins(self):
135
+ """Load all plugins from plugin directory"""
136
+ for file in Path(self.plugin_dir).glob("*.py"):
137
+ if file.name.startswith("_"):
138
+ continue
139
+ try:
140
+ module = importlib.import_module(file.stem)
141
+ for attr_name in dir(module):
142
+ attr = getattr(module, attr_name)
143
+ if (isinstance(attr, type) and
144
+ issubclass(attr, Plugin) and
145
+ attr is not Plugin):
146
+ plugin = attr()
147
+ self.register(plugin)
148
+ except Exception as e:
149
+ console.print(f"[red]Error loading plugin {file.name}: {e}[/red]")
150
+
151
+ def before_command(self, command: str, args: List[str]) -> bool:
152
+ """Run before_command hooks"""
153
+ for plugin in self.plugins:
154
+ if not plugin.enabled:
155
+ continue
156
+ try:
157
+ result = plugin.before_command(command, args)
158
+ if result is False:
159
+ return False
160
+ except Exception as e:
161
+ console.print(f"[red]Error in plugin {plugin.__class__.__name__}: {e}[/red]")
162
+ return True
163
+
164
+ def after_command(self, command: str, args: List[str], result: Any):
165
+ """Run after_command hooks"""
166
+ for plugin in self.plugins:
167
+ if not plugin.enabled:
168
+ continue
169
+ try:
170
+ plugin.after_command(command, args, result)
171
+ except Exception as e:
172
+ console.print(f"[red]Error in plugin {plugin.__class__.__name__}: {e}[/red]")
173
+
174
+ def on_error(self, command: str, error: Exception):
175
+ """Run error hooks"""
176
+ for plugin in self.plugins:
177
+ if not plugin.enabled:
178
+ continue
179
+ try:
180
+ plugin.on_error(command, error)
181
+ except Exception as e:
182
+ console.print(f"[red]Error in plugin {plugin.__class__.__name__}: {e}[/red]")
183
+
184
+ class Group:
185
+ """
186
+ Command group that can contain subcommands and be chained.
187
+
188
+ Basic Usage:
189
+ >>> @app.group()
190
+ ... def db():
191
+ ... '''Database commands'''
192
+ ... pass
193
+ >>> @db.command()
194
+ ... def migrate():
195
+ ... '''Run migrations'''
196
+ ... pass
197
+
198
+ Advanced Usage:
199
+ >>> @app.group(chain=True)
200
+ ... def process():
201
+ ... '''Process data'''
202
+ ... pass
203
+ >>> @process.command()
204
+ ... def validate():
205
+ ... '''Validate data'''
206
+ ... pass
207
+ """
208
+ def __init__(
209
+ self,
210
+ name: str = None,
211
+ help: str = None,
212
+ chain: bool = False,
213
+ invoke_without_command: bool = False
214
+ ):
215
+ self.name = name
216
+ self.help = help
217
+ self.chain = chain
218
+ self.invoke_without_command = invoke_without_command
219
+ self.commands = {}
220
+
221
+ def command(
222
+ self,
223
+ name: str = None,
224
+ help: str = None,
225
+ aliases: List[str] = None,
226
+ hidden: bool = False
227
+ ):
228
+ """Register a new command"""
229
+ def decorator(f):
230
+ cmd_name = name or f.__name__
231
+ self.commands[cmd_name] = {
232
+ 'func': f,
233
+ 'help': help or f.__doc__,
234
+ 'aliases': aliases or [],
235
+ 'hidden': hidden
236
+ }
237
+ return f
238
+ return decorator
239
+
240
+ def group(self, *args, **kwargs):
241
+ """Create a subgroup"""
242
+ def decorator(f):
243
+ subgroup = Group(*args, **kwargs)
244
+ self.commands[subgroup.name] = subgroup
245
+ return subgroup
246
+ return decorator
247
+
248
+ def run(self, args: List[str]):
249
+ """Run the group command"""
250
+ if not args or args[0] in ['-h', '--help']:
251
+ self._print_help()
252
+ return
253
+
254
+ command_name = args[0]
255
+ command_args = args[1:]
256
+
257
+ if command_name not in self.commands:
258
+ console.print(f"[red]Unknown command: {command_name}[/red]")
259
+ self._print_help()
260
+ return 1
261
+
262
+ command = self.commands[command_name]
263
+ try:
264
+ result = command['func'](**self._parse_args(command, command_args))
265
+ if self.chain and result is not None:
266
+ return result
267
+ except Exception as e:
268
+ console.print(f"[red]Error: {str(e)}[/red]")
269
+ return 1
270
+
271
+ def _parse_args(self, command: Dict, args: List[str]) -> Dict[str, Any]:
272
+ """Parse command arguments"""
273
+ params = {}
274
+ func = command['func']
275
+ sig = inspect.signature(func)
276
+
277
+ # Handle options
278
+ if hasattr(func, '_options'):
279
+ for opt in func._options:
280
+ # Get the destination parameter name from the longest option
281
+ param_decls = sorted(opt['param_decls'], key=len, reverse=True)
282
+ param_name = param_decls[0].lstrip('-').replace('-', '_')
283
+
284
+ # If there's a parameter name in the signature, use that instead
285
+ for param in sig.parameters.values():
286
+ if param.name in [d.lstrip('-').replace('-', '_') for d in param_decls]:
287
+ param_name = param.name
288
+ break
289
+
290
+ found = False
291
+ multiple_values = []
292
+
293
+ # Check for long and short options
294
+ i = 0
295
+ while i < len(args):
296
+ if args[i] in opt['param_decls']:
297
+ if opt.get('is_flag', False):
298
+ if opt.get('multiple', False):
299
+ multiple_values.append(True)
300
+ else:
301
+ params[param_name] = True
302
+ else:
303
+ if i + 1 < len(args):
304
+ value = args[i + 1]
305
+ # Convert value to the correct type
306
+ if 'type' in opt:
307
+ try:
308
+ value = opt['type'](value)
309
+ except ValueError:
310
+ raise UsageError(f"Invalid value for {args[i]}: {value}")
311
+
312
+ if opt.get('multiple', False):
313
+ multiple_values.append(value)
314
+ else:
315
+ params[param_name] = value
316
+ args.pop(i + 1)
317
+ else:
318
+ raise UsageError(f"Option {args[i]} requires a value")
319
+ args.pop(i)
320
+ found = True
321
+ if not opt.get('multiple', False):
322
+ break
323
+ else:
324
+ i += 1
325
+
326
+ # Set multiple values if any
327
+ if multiple_values:
328
+ params[param_name] = multiple_values
329
+
330
+ # Handle required options
331
+ if not found and opt.get('required', False):
332
+ raise UsageError(f"Option {opt['param_decls'][0]} is required")
333
+
334
+ # Set default value if not found
335
+ if not found and 'default' in opt:
336
+ params[param_name] = opt['default']
337
+
338
+ # Handle arguments
339
+ if hasattr(func, '_arguments'):
340
+ for i, arg in enumerate(func._arguments):
341
+ if i < len(args):
342
+ value = args[i]
343
+ # Convert value to the correct type
344
+ if 'type' in arg:
345
+ try:
346
+ value = arg['type'](value)
347
+ except ValueError:
348
+ raise UsageError(f"Invalid value for {arg['name']}: {value}")
349
+ params[arg['name']] = value
350
+ elif arg.get('required', True):
351
+ raise UsageError(f"Argument {arg['name']} is required")
352
+ elif 'default' in arg:
353
+ params[arg['name']] = arg['default']
354
+
355
+ # Handle environment variables
356
+ if hasattr(func, '_envvars'):
357
+ for env in func._envvars:
358
+ value = os.environ.get(env['name'])
359
+ if env.get('required', False) and not value:
360
+ raise UsageError(f"Environment variable {env['name']} is required")
361
+ if value:
362
+ # Convert value to the correct type
363
+ if 'type' in env:
364
+ try:
365
+ value = env['type'](value)
366
+ except ValueError:
367
+ raise UsageError(f"Invalid value for {env['name']}: {value}")
368
+ params[env['name'].lower()] = value
369
+
370
+ return params
371
+
372
+ def _print_help(self):
373
+ """Print group help message"""
374
+ console.print(f"\n{self.name} commands:")
375
+ if self.help:
376
+ console.print(f"\n{self.help}")
377
+
378
+ for name, cmd in self.commands.items():
379
+ if not cmd.get('hidden', False):
380
+ console.print(f" {name:20} {cmd['help'] or ''}")
381
+
382
+ console.print("\nUse -h or --help with any command for more info")
383
+
384
+ class CLI:
385
+ """
386
+ The main CLI application class that handles all command registration and execution.
387
+
388
+ Basic Usage:
389
+ >>> from swiftcli import CLI
390
+ >>> app = CLI("myapp")
391
+ >>> @app.command()
392
+ ... def greet(name: str):
393
+ ... print(f"Hello {name}!")
394
+ >>> app.run()
395
+
396
+ Advanced Usage:
397
+ >>> app = CLI("myapp", version="1.0.0")
398
+ >>> @app.group()
399
+ ... def config():
400
+ ... '''Manage configuration'''
401
+ ... pass
402
+ >>> @config.command()
403
+ ... def set(key: str, value: str):
404
+ ... '''Set config value'''
405
+ ... print(f"Setting {key}={value}")
406
+ """
407
+ def __init__(
408
+ self,
409
+ name: str = None,
410
+ help: str = None,
411
+ version: str = None,
412
+ chain: bool = False
413
+ ):
414
+ self.name = name
415
+ self.help = help
416
+ self.version = version
417
+ self.chain = chain
418
+ self.commands = {}
419
+ self.groups = {}
420
+ self.plugin_manager = PluginManager()
421
+
422
+ def command(
423
+ self,
424
+ name: str = None,
425
+ help: str = None,
426
+ aliases: List[str] = None,
427
+ hidden: bool = False
428
+ ):
429
+ """
430
+ Decorator to register a new command.
431
+
432
+ Basic Usage:
433
+ >>> @app.command()
434
+ ... def hello(name: str):
435
+ ... '''Say hello'''
436
+ ... print(f"Hello {name}!")
437
+
438
+ Advanced Usage:
439
+ >>> @app.command(name="greet", aliases=["hi", "hey"])
440
+ ... def hello(name: str):
441
+ ... '''Greet someone'''
442
+ ... print(f"Hello {name}!")
443
+ """
444
+ def decorator(f):
445
+ cmd_name = name or f.__name__
446
+ self.commands[cmd_name] = {
447
+ 'func': f,
448
+ 'help': help or f.__doc__,
449
+ 'aliases': aliases or [],
450
+ 'hidden': hidden
451
+ }
452
+ return f
453
+ return decorator
454
+
455
+ def group(
456
+ self,
457
+ name: str = None,
458
+ help: str = None,
459
+ chain: bool = False,
460
+ **kwargs
461
+ ):
462
+ """Create a command group"""
463
+ def decorator(f):
464
+ if hasattr(f, '_group'):
465
+ group_info = f._group
466
+ group = Group(
467
+ name=group_info['name'],
468
+ help=group_info['help'],
469
+ chain=group_info['chain'],
470
+ invoke_without_command=group_info['invoke_without_command']
471
+ )
472
+ else:
473
+ group = Group(
474
+ name=name or f.__name__,
475
+ help=help or f.__doc__,
476
+ chain=chain
477
+ )
478
+ self.groups[group.name] = group
479
+ return group
480
+ return decorator
481
+
482
+ def run(self, args: List[str] = None):
483
+ """Run the CLI application"""
484
+ args = args or sys.argv[1:]
485
+
486
+ if not args or args[0] in ['-h', '--help']:
487
+ self._print_help()
488
+ return
489
+
490
+ if args[0] in ['-v', '--version'] and self.version:
491
+ console.print(self.version)
492
+ return
493
+
494
+ command_name = args[0]
495
+ command_args = args[1:]
496
+
497
+ # Check if it's a group command
498
+ if command_name in self.groups:
499
+ group = self.groups[command_name]
500
+ if len(command_args) == 0:
501
+ if not group.invoke_without_command:
502
+ group._print_help()
503
+ return
504
+ else:
505
+ return group.run(command_args)
506
+
507
+ # Regular command
508
+ if command_name not in self.commands:
509
+ console.print(f"[red]Unknown command: {command_name}[/red]")
510
+ self._print_help()
511
+ return 1
512
+
513
+ command = self.commands[command_name]
514
+ try:
515
+ ctx = Context(self, command=command_name)
516
+ result = command['func'](**self._parse_args(command, command_args))
517
+
518
+ if self.chain and result is not None:
519
+ return result
520
+
521
+ except Exception as e:
522
+ console.print(f"[red]Error: {str(e)}[/red]")
523
+ return 1
524
+
525
+ def _parse_args(self, command: Dict, args: List[str]) -> Dict[str, Any]:
526
+ """Parse command arguments"""
527
+ params = {}
528
+ func = command['func']
529
+ sig = inspect.signature(func)
530
+
531
+ # Handle options
532
+ if hasattr(func, '_options'):
533
+ for opt in func._options:
534
+ # Get the destination parameter name from the longest option
535
+ param_decls = sorted(opt['param_decls'], key=len, reverse=True)
536
+ param_name = param_decls[0].lstrip('-').replace('-', '_')
537
+
538
+ # If there's a parameter name in the signature, use that instead
539
+ for param in sig.parameters.values():
540
+ if param.name in [d.lstrip('-').replace('-', '_') for d in param_decls]:
541
+ param_name = param.name
542
+ break
543
+
544
+ found = False
545
+ multiple_values = []
546
+
547
+ # Check for long and short options
548
+ i = 0
549
+ while i < len(args):
550
+ if args[i] in opt['param_decls']:
551
+ if opt.get('is_flag', False):
552
+ if opt.get('multiple', False):
553
+ multiple_values.append(True)
554
+ else:
555
+ params[param_name] = True
556
+ else:
557
+ if i + 1 < len(args):
558
+ value = args[i + 1]
559
+ # Convert value to the correct type
560
+ if 'type' in opt:
561
+ try:
562
+ value = opt['type'](value)
563
+ except ValueError:
564
+ raise UsageError(f"Invalid value for {args[i]}: {value}")
565
+
566
+ if opt.get('multiple', False):
567
+ multiple_values.append(value)
568
+ else:
569
+ params[param_name] = value
570
+ args.pop(i + 1)
571
+ else:
572
+ raise UsageError(f"Option {args[i]} requires a value")
573
+ args.pop(i)
574
+ found = True
575
+ if not opt.get('multiple', False):
576
+ break
577
+ else:
578
+ i += 1
579
+
580
+ # Set multiple values if any
581
+ if multiple_values:
582
+ params[param_name] = multiple_values
583
+
584
+ # Handle required options
585
+ if not found and opt.get('required', False):
586
+ raise UsageError(f"Option {opt['param_decls'][0]} is required")
587
+
588
+ # Set default value if not found
589
+ if not found and 'default' in opt:
590
+ params[param_name] = opt['default']
591
+
592
+ # Handle arguments
593
+ if hasattr(func, '_arguments'):
594
+ for i, arg in enumerate(func._arguments):
595
+ if i < len(args):
596
+ value = args[i]
597
+ # Convert value to the correct type
598
+ if 'type' in arg:
599
+ try:
600
+ value = arg['type'](value)
601
+ except ValueError:
602
+ raise UsageError(f"Invalid value for {arg['name']}: {value}")
603
+ params[arg['name']] = value
604
+ elif arg.get('required', True):
605
+ raise UsageError(f"Argument {arg['name']} is required")
606
+ elif 'default' in arg:
607
+ params[arg['name']] = arg['default']
608
+
609
+ # Handle environment variables
610
+ if hasattr(func, '_envvars'):
611
+ for env in func._envvars:
612
+ value = os.environ.get(env['name'])
613
+ if env.get('required', False) and not value:
614
+ raise UsageError(f"Environment variable {env['name']} is required")
615
+ if value:
616
+ # Convert value to the correct type
617
+ if 'type' in env:
618
+ try:
619
+ value = env['type'](value)
620
+ except ValueError:
621
+ raise UsageError(f"Invalid value for {env['name']}: {value}")
622
+ params[env['name'].lower()] = value
623
+
624
+ return params
625
+
626
+ def _print_help(self):
627
+ """Print main help message"""
628
+ console.print(f"\n{self.name or 'CLI Application'}")
629
+ if self.help:
630
+ console.print(f"\n{self.help}")
631
+
632
+ console.print("\nCommands:")
633
+ for name, cmd in self.commands.items():
634
+ if not cmd.get('hidden', False):
635
+ console.print(f" {name:20} {cmd['help'] or ''}")
636
+
637
+ for name, group in self.groups.items():
638
+ console.print(f"\n{name} commands:")
639
+ for cmd_name, cmd in group.commands.items():
640
+ if not cmd.get('hidden', False):
641
+ console.print(f" {name} {cmd_name:20} {cmd['help'] or ''}")
642
+
643
+ console.print("\nUse -h or --help with any command for more info")
644
+
645
+ def command(
646
+ name: str = None,
647
+ help: str = None,
648
+ aliases: List[str] = None,
649
+ hidden: bool = False
650
+ ):
651
+ """
652
+ Decorator to register a new command.
653
+
654
+ Basic Usage:
655
+ >>> @app.command()
656
+ ... def hello(name: str):
657
+ ... '''Say hello'''
658
+ ... print(f"Hello {name}!")
659
+
660
+ Advanced Usage:
661
+ >>> @app.command(name="greet", aliases=["hi", "hey"])
662
+ ... def hello(name: str):
663
+ ... '''Greet someone'''
664
+ ... print(f"Hello {name}!")
665
+ """
666
+ def decorator(f: Callable) -> Callable:
667
+ f._command = {
668
+ 'name': name or f.__name__,
669
+ 'help': help or f.__doc__,
670
+ 'aliases': aliases or [],
671
+ 'hidden': hidden
672
+ }
673
+ return f
674
+ return decorator
675
+
676
+ def option(*param_decls, **attrs):
677
+ """
678
+ Decorator to add an option to a command.
679
+
680
+ Basic Usage:
681
+ >>> @app.command()
682
+ ... @option("--count", type=int, default=1)
683
+ ... def repeat(count: int, message: str):
684
+ ... '''Repeat a message'''
685
+ ... for _ in range(count):
686
+ ... print(message)
687
+
688
+ Advanced Usage:
689
+ >>> @app.command()
690
+ ... @option("--format", "-f", type=click.Choice(["json", "yaml"]))
691
+ ... def export(format: str):
692
+ ... '''Export data'''
693
+ ... print(f"Exporting as {format}")
694
+ """
695
+ def decorator(f: Callable) -> Callable:
696
+ if not hasattr(f, '_options'):
697
+ f._options = []
698
+
699
+ # Set default values
700
+ attrs.setdefault('type', str)
701
+ attrs.setdefault('required', False)
702
+ attrs.setdefault('default', None)
703
+ attrs.setdefault('help', None)
704
+ attrs.setdefault('is_flag', False)
705
+ attrs.setdefault('multiple', False)
706
+ attrs.setdefault('count', False)
707
+ attrs.setdefault('prompt', False)
708
+ attrs.setdefault('hide_input', False)
709
+ attrs.setdefault('confirmation_prompt', False)
710
+ attrs.setdefault('choices', None)
711
+ attrs.setdefault('callback', None)
712
+ attrs.setdefault('show_default', True)
713
+ attrs.setdefault('hidden', False)
714
+
715
+ f._options.append({
716
+ 'param_decls': param_decls,
717
+ **attrs
718
+ })
719
+ return f
720
+ return decorator
721
+
722
+ def argument(name: str, **attrs):
723
+ """Argument decorator"""
724
+ def decorator(f: Callable) -> Callable:
725
+ if not hasattr(f, '_arguments'):
726
+ f._arguments = []
727
+ f._arguments.append({
728
+ 'name': name,
729
+ **attrs
730
+ })
731
+ return f
732
+ return decorator
733
+
734
+ def group(
735
+ name: str = None,
736
+ help: str = None,
737
+ chain: bool = False,
738
+ invoke_without_command: bool = False
739
+ ):
740
+ """Group decorator"""
741
+ def decorator(f: Callable) -> Callable:
742
+ f._group = {
743
+ 'name': name or f.__name__,
744
+ 'help': help or f.__doc__,
745
+ 'chain': chain,
746
+ 'invoke_without_command': invoke_without_command
747
+ }
748
+ return f
749
+ return decorator
750
+
751
+ def pass_context(f: Callable) -> Callable:
752
+ """Pass context decorator"""
753
+ f._pass_context = True
754
+ return f
755
+
756
+ def envvar(name: str, help: str = None, required: bool = False):
757
+ """Environment variable decorator"""
758
+ def decorator(f: Callable) -> Callable:
759
+ if not hasattr(f, '_envvars'):
760
+ f._envvars = []
761
+ f._envvars.append({
762
+ 'name': name,
763
+ 'help': help,
764
+ 'required': required
765
+ })
766
+ return f
767
+ return decorator
768
+
769
+ def config_file(path: str = None, auto_create: bool = True):
770
+ """Configuration file decorator"""
771
+ def decorator(f: Callable) -> Callable:
772
+ f._config = {
773
+ 'path': path,
774
+ 'auto_create': auto_create
775
+ }
776
+ return f
777
+ return decorator
778
+
779
+ def table_output(headers: List[str], style: str = None):
780
+ """Table output decorator"""
781
+ def decorator(f: Callable) -> Callable:
782
+ @wraps(f)
783
+ def wrapper(*args, **kwargs):
784
+ result = f(*args, **kwargs)
785
+ if result:
786
+ table = Table(show_header=True, header_style="bold blue")
787
+ for header in headers:
788
+ table.add_column(header)
789
+ for row in result:
790
+ table.add_row(*[str(cell) for cell in row])
791
+ console.print(table)
792
+ return result
793
+ return wrapper
794
+ return decorator
795
+
796
+ def progress(description: str = None):
797
+ """Progress decorator"""
798
+ def decorator(f: Callable) -> Callable:
799
+ @wraps(f)
800
+ def wrapper(*args, **kwargs):
801
+ with Progress(
802
+ SpinnerColumn(),
803
+ TextColumn("[progress.description]{task.description}"),
804
+ transient=True,
805
+ ) as progress:
806
+ task = progress.add_task(description or f.__name__, total=None)
807
+ result = f(*args, **kwargs)
808
+ progress.update(task, completed=True)
809
+ return result
810
+ return wrapper
811
+ return decorator