hammad-python 0.0.15__py3-none-any.whl → 0.0.17__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.
Files changed (111) hide show
  1. hammad/__init__.py +178 -0
  2. hammad/_internal.py +237 -0
  3. hammad/cache/__init__.py +40 -0
  4. hammad/cache/base_cache.py +181 -0
  5. hammad/cache/cache.py +169 -0
  6. hammad/cache/decorators.py +261 -0
  7. hammad/cache/file_cache.py +80 -0
  8. hammad/cache/ttl_cache.py +74 -0
  9. hammad/cli/__init__.py +35 -0
  10. hammad/cli/_runner.py +265 -0
  11. hammad/cli/animations.py +573 -0
  12. hammad/cli/plugins.py +836 -0
  13. hammad/cli/styles/__init__.py +55 -0
  14. hammad/cli/styles/settings.py +139 -0
  15. hammad/cli/styles/types.py +358 -0
  16. hammad/cli/styles/utils.py +626 -0
  17. hammad/data/__init__.py +83 -0
  18. hammad/data/collections/__init__.py +44 -0
  19. hammad/data/collections/collection.py +274 -0
  20. hammad/data/collections/indexes/__init__.py +37 -0
  21. hammad/data/collections/indexes/qdrant/__init__.py +1 -0
  22. hammad/data/collections/indexes/qdrant/index.py +735 -0
  23. hammad/data/collections/indexes/qdrant/settings.py +94 -0
  24. hammad/data/collections/indexes/qdrant/utils.py +220 -0
  25. hammad/data/collections/indexes/tantivy/__init__.py +1 -0
  26. hammad/data/collections/indexes/tantivy/index.py +428 -0
  27. hammad/data/collections/indexes/tantivy/settings.py +51 -0
  28. hammad/data/collections/indexes/tantivy/utils.py +200 -0
  29. hammad/data/configurations/__init__.py +35 -0
  30. hammad/data/configurations/configuration.py +564 -0
  31. hammad/data/models/__init__.py +55 -0
  32. hammad/data/models/extensions/__init__.py +4 -0
  33. hammad/data/models/extensions/pydantic/__init__.py +42 -0
  34. hammad/data/models/extensions/pydantic/converters.py +759 -0
  35. hammad/data/models/fields.py +546 -0
  36. hammad/data/models/model.py +1078 -0
  37. hammad/data/models/utils.py +280 -0
  38. hammad/data/sql/__init__.py +23 -0
  39. hammad/data/sql/database.py +578 -0
  40. hammad/data/sql/types.py +141 -0
  41. hammad/data/types/__init__.py +39 -0
  42. hammad/data/types/file.py +358 -0
  43. hammad/data/types/multimodal/__init__.py +24 -0
  44. hammad/data/types/multimodal/audio.py +96 -0
  45. hammad/data/types/multimodal/image.py +80 -0
  46. hammad/data/types/text.py +1066 -0
  47. hammad/formatting/__init__.py +20 -0
  48. hammad/formatting/json/__init__.py +27 -0
  49. hammad/formatting/json/converters.py +158 -0
  50. hammad/formatting/text/__init__.py +63 -0
  51. hammad/formatting/text/converters.py +723 -0
  52. hammad/formatting/text/markdown.py +131 -0
  53. hammad/formatting/yaml/__init__.py +26 -0
  54. hammad/formatting/yaml/converters.py +5 -0
  55. hammad/genai/__init__.py +78 -0
  56. hammad/genai/agents/__init__.py +1 -0
  57. hammad/genai/agents/types/__init__.py +35 -0
  58. hammad/genai/agents/types/history.py +277 -0
  59. hammad/genai/agents/types/tool.py +490 -0
  60. hammad/genai/embedding_models/__init__.py +41 -0
  61. hammad/genai/embedding_models/embedding_model.py +193 -0
  62. hammad/genai/embedding_models/embedding_model_name.py +77 -0
  63. hammad/genai/embedding_models/embedding_model_request.py +65 -0
  64. hammad/genai/embedding_models/embedding_model_response.py +69 -0
  65. hammad/genai/embedding_models/run.py +161 -0
  66. hammad/genai/language_models/__init__.py +35 -0
  67. hammad/genai/language_models/_streaming.py +622 -0
  68. hammad/genai/language_models/_types.py +276 -0
  69. hammad/genai/language_models/_utils/__init__.py +31 -0
  70. hammad/genai/language_models/_utils/_completions.py +131 -0
  71. hammad/genai/language_models/_utils/_messages.py +89 -0
  72. hammad/genai/language_models/_utils/_requests.py +202 -0
  73. hammad/genai/language_models/_utils/_structured_outputs.py +124 -0
  74. hammad/genai/language_models/language_model.py +734 -0
  75. hammad/genai/language_models/language_model_request.py +135 -0
  76. hammad/genai/language_models/language_model_response.py +219 -0
  77. hammad/genai/language_models/language_model_response_chunk.py +53 -0
  78. hammad/genai/language_models/run.py +530 -0
  79. hammad/genai/multimodal_models.py +48 -0
  80. hammad/genai/rerank_models.py +26 -0
  81. hammad/logging/__init__.py +35 -0
  82. hammad/logging/decorators.py +834 -0
  83. hammad/logging/logger.py +954 -0
  84. hammad/mcp/__init__.py +50 -0
  85. hammad/mcp/client/__init__.py +36 -0
  86. hammad/mcp/client/client.py +624 -0
  87. hammad/mcp/client/client_service.py +400 -0
  88. hammad/mcp/client/settings.py +178 -0
  89. hammad/mcp/servers/__init__.py +25 -0
  90. hammad/mcp/servers/launcher.py +1161 -0
  91. hammad/runtime/__init__.py +32 -0
  92. hammad/runtime/decorators.py +142 -0
  93. hammad/runtime/run.py +299 -0
  94. hammad/service/__init__.py +49 -0
  95. hammad/service/create.py +527 -0
  96. hammad/service/decorators.py +285 -0
  97. hammad/typing/__init__.py +435 -0
  98. hammad/web/__init__.py +43 -0
  99. hammad/web/http/__init__.py +1 -0
  100. hammad/web/http/client.py +944 -0
  101. hammad/web/models.py +277 -0
  102. hammad/web/openapi/__init__.py +1 -0
  103. hammad/web/openapi/client.py +740 -0
  104. hammad/web/search/__init__.py +1 -0
  105. hammad/web/search/client.py +1035 -0
  106. hammad/web/utils.py +472 -0
  107. {hammad_python-0.0.15.dist-info → hammad_python-0.0.17.dist-info}/METADATA +8 -1
  108. hammad_python-0.0.17.dist-info/RECORD +110 -0
  109. hammad_python-0.0.15.dist-info/RECORD +0 -4
  110. {hammad_python-0.0.15.dist-info → hammad_python-0.0.17.dist-info}/WHEEL +0 -0
  111. {hammad_python-0.0.15.dist-info → hammad_python-0.0.17.dist-info}/licenses/LICENSE +0 -0
hammad/cli/plugins.py ADDED
@@ -0,0 +1,836 @@
1
+ """hammad.cli.plugins
2
+
3
+ Contains the following 'builtin' plugins or extensions:
4
+
5
+ - `print()`
6
+ - `input()`
7
+ - `animate()`
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import builtins
13
+ import json
14
+ from typing import (
15
+ Optional,
16
+ IO,
17
+ overload,
18
+ Any,
19
+ Dict,
20
+ Literal,
21
+ List,
22
+ Union,
23
+ Callable,
24
+ TYPE_CHECKING,
25
+ )
26
+
27
+ if TYPE_CHECKING:
28
+ from rich import get_console
29
+ from rich.console import (
30
+ JustifyMethod,
31
+ OverflowMethod,
32
+ Console,
33
+ RenderableType,
34
+ )
35
+ from rich.panel import PaddingDimensions
36
+ from rich.prompt import Prompt, Confirm
37
+ from prompt_toolkit import prompt as pt_prompt
38
+ from prompt_toolkit.completion import WordCompleter
39
+ from .animations import (
40
+ CLIFlashingAnimation,
41
+ CLIPulsingAnimation,
42
+ CLIShakingAnimation,
43
+ CLITypingAnimation,
44
+ CLISpinningAnimation,
45
+ CLIRainbowAnimation,
46
+ RainbowPreset,
47
+ )
48
+ from .styles.types import (
49
+ CLIStyleType,
50
+ CLIStyleBackgroundType,
51
+ CLIStyleColorName,
52
+ CLIStyleBoxName,
53
+ )
54
+ from .styles.settings import (
55
+ CLIStyleRenderableSettings,
56
+ CLIStyleBackgroundSettings,
57
+ CLIStyleLiveSettings,
58
+ )
59
+ from .styles.utils import (
60
+ live_render,
61
+ style_renderable,
62
+ )
63
+
64
+ # Lazy import cache
65
+ _IMPORT_CACHE = {}
66
+
67
+
68
+ def _get_rich_console():
69
+ """Lazy import for rich.get_console"""
70
+ if "get_console" not in _IMPORT_CACHE:
71
+ from rich import get_console
72
+
73
+ _IMPORT_CACHE["get_console"] = get_console
74
+ return _IMPORT_CACHE["get_console"]
75
+
76
+
77
+ def _get_rich_console_classes():
78
+ """Lazy import for rich.console classes"""
79
+ if "console_classes" not in _IMPORT_CACHE:
80
+ from rich.console import Console, RenderableType
81
+
82
+ _IMPORT_CACHE["console_classes"] = (Console, RenderableType)
83
+ return _IMPORT_CACHE["console_classes"]
84
+
85
+
86
+ def _get_rich_prompts():
87
+ """Lazy import for rich.prompt classes"""
88
+ if "prompts" not in _IMPORT_CACHE:
89
+ from rich.prompt import Prompt, Confirm
90
+
91
+ _IMPORT_CACHE["prompts"] = (Prompt, Confirm)
92
+ return _IMPORT_CACHE["prompts"]
93
+
94
+
95
+ def _get_prompt_toolkit():
96
+ """Lazy import for prompt_toolkit"""
97
+ if "prompt_toolkit" not in _IMPORT_CACHE:
98
+ from prompt_toolkit import prompt as pt_prompt
99
+ from prompt_toolkit.completion import WordCompleter
100
+
101
+ _IMPORT_CACHE["prompt_toolkit"] = (pt_prompt, WordCompleter)
102
+ return _IMPORT_CACHE["prompt_toolkit"]
103
+
104
+
105
+ def _get_style_utils():
106
+ """Lazy import for style utilities"""
107
+ if "style_utils" not in _IMPORT_CACHE:
108
+ from .styles.utils import live_render, style_renderable
109
+
110
+ _IMPORT_CACHE["style_utils"] = (live_render, style_renderable)
111
+ return _IMPORT_CACHE["style_utils"]
112
+
113
+
114
+ def _get_animation_classes():
115
+ """Lazy import for animation classes"""
116
+ if "animations" not in _IMPORT_CACHE:
117
+ from .animations import (
118
+ CLIFlashingAnimation,
119
+ CLIPulsingAnimation,
120
+ CLIShakingAnimation,
121
+ CLITypingAnimation,
122
+ CLISpinningAnimation,
123
+ CLIRainbowAnimation,
124
+ RainbowPreset,
125
+ )
126
+
127
+ _IMPORT_CACHE["animations"] = {
128
+ "CLIFlashingAnimation": CLIFlashingAnimation,
129
+ "CLIPulsingAnimation": CLIPulsingAnimation,
130
+ "CLIShakingAnimation": CLIShakingAnimation,
131
+ "CLITypingAnimation": CLITypingAnimation,
132
+ "CLISpinningAnimation": CLISpinningAnimation,
133
+ "CLIRainbowAnimation": CLIRainbowAnimation,
134
+ "RainbowPreset": RainbowPreset,
135
+ }
136
+ return _IMPORT_CACHE["animations"]
137
+
138
+
139
+ def print(
140
+ *values: object,
141
+ sep: str = " ",
142
+ end: str = "\n",
143
+ file: Optional[IO[str]] = None,
144
+ flush: bool = False,
145
+ style: "CLIStyleType | None" = None,
146
+ style_settings: "CLIStyleRenderableSettings | None" = None,
147
+ bg: "CLIStyleBackgroundType | None" = None,
148
+ bg_settings: "CLIStyleBackgroundSettings | None" = None,
149
+ justify: Optional["JustifyMethod"] = None,
150
+ overflow: Optional["OverflowMethod"] = None,
151
+ no_wrap: Optional[bool] = None,
152
+ emoji: Optional[bool] = None,
153
+ markup: Optional[bool] = None,
154
+ highlight: Optional[bool] = None,
155
+ width: Optional[int] = None,
156
+ height: Optional[int] = None,
157
+ border: Optional["CLIStyleBoxName"] = None,
158
+ padding: Optional["PaddingDimensions"] = None,
159
+ title: Optional[str] = None,
160
+ expand: Optional[bool] = None,
161
+ live: "CLIStyleLiveSettings | int | None" = None,
162
+ transient: bool = False,
163
+ new_line_start: bool = False,
164
+ ) -> None:
165
+ """
166
+ Stylized print function built with `rich`. This method maintains
167
+ all standard functionality of the print function, with no overhead
168
+ unless the styled parameters are provided.
169
+
170
+ Args:
171
+ *values : The values to print.
172
+ sep : The separator between values.
173
+ end : The end character.
174
+ file : The file to write to.
175
+ flush : Whether to flush the file.
176
+ style : A color or style name to apply to the content.
177
+ style_settings : A dictionary of style settings to apply to the content.
178
+ bg : A color or box name to apply to the background.
179
+ bg_settings : A dictionary of background settings to apply to the content.
180
+ justify : Text justification method ("left", "center", "right", "full").
181
+ overflow : Text overflow method ("fold", "crop", "ellipsis", "ignore").
182
+ no_wrap : Disable text wrapping.
183
+ emoji : Enable/disable emoji rendering.
184
+ markup : Enable/disable Rich markup rendering.
185
+ highlight : Enable/disable automatic highlighting.
186
+ width : Override the width of the output.
187
+ height : Override the height of the output.
188
+ border : Border style for panel rendering.
189
+ padding : Padding dimensions for panel rendering.
190
+ title : Title for panel rendering.
191
+ expand : Whether to expand panel to full width.
192
+ live : A dictionary of live settings or an integer in seconds to run the print in a live renderable.
193
+ transient : Whether to clear the output after completion.
194
+ new_line_start : Start with a new line before printing.
195
+
196
+ NOTE: If `live` is set as an integer, transient is True.
197
+
198
+ Returns:
199
+ None
200
+
201
+ Raises:
202
+ PrintError : If the renderable is not a RenderableType.
203
+ """
204
+
205
+ # If no styling parameters are provided, use built-in print to avoid rich's default styling
206
+ if (
207
+ style is None
208
+ and style_settings is None
209
+ and bg is None
210
+ and bg_settings is None
211
+ and live is None
212
+ and justify is None
213
+ and overflow is None
214
+ and no_wrap is None
215
+ and emoji is None
216
+ and markup is None
217
+ and highlight is None
218
+ and width is None
219
+ and height is None
220
+ and border is None
221
+ and padding is None
222
+ and title is None
223
+ and expand is None
224
+ ):
225
+ builtins.print(*values, sep=sep, end=end, file=file, flush=flush)
226
+ return
227
+
228
+ # Convert values to string for styling
229
+ content = sep.join(str(value) for value in values)
230
+
231
+ # Apply styling and background
232
+ live_render, style_renderable = _get_style_utils()
233
+ styled_content = style_renderable(
234
+ content,
235
+ style=style,
236
+ style_settings=style_settings,
237
+ bg=bg,
238
+ bg_settings=bg_settings,
239
+ border=border,
240
+ padding=padding,
241
+ title=title,
242
+ expand=expand,
243
+ )
244
+
245
+ # Handle live rendering
246
+ if live is not None:
247
+ if isinstance(live, int):
248
+ # If live is an integer, treat it as duration in seconds
249
+ from .styles.settings import CLIStyleLiveSettings
250
+
251
+ live_settings: CLIStyleLiveSettings = {
252
+ "duration": float(live),
253
+ "transient": False, # Changed to False for testing
254
+ }
255
+ else:
256
+ live_settings = live
257
+
258
+ # For very short durations or testing, just print normally
259
+ duration = live if isinstance(live, int) else live_settings.get("duration", 2.0)
260
+ if duration <= 1:
261
+ get_console = _get_rich_console()
262
+ Console, _ = _get_rich_console_classes()
263
+ console = get_console() if file is None else Console(file=file)
264
+ console.print(
265
+ styled_content,
266
+ end=end,
267
+ justify=justify,
268
+ overflow=overflow,
269
+ no_wrap=no_wrap,
270
+ emoji=emoji,
271
+ markup=markup,
272
+ highlight=highlight,
273
+ width=width,
274
+ height=height,
275
+ new_line_start=new_line_start,
276
+ )
277
+ else:
278
+ live_render(styled_content, live_settings)
279
+ else:
280
+ # Regular print with styling
281
+ get_console = _get_rich_console()
282
+ Console, _ = _get_rich_console_classes()
283
+ console = get_console() if file is None else Console(file=file)
284
+ console.print(
285
+ styled_content,
286
+ end=end,
287
+ justify=justify,
288
+ overflow=overflow,
289
+ no_wrap=no_wrap,
290
+ emoji=emoji,
291
+ markup=markup,
292
+ highlight=highlight,
293
+ width=width,
294
+ height=height,
295
+ new_line_start=new_line_start,
296
+ )
297
+
298
+
299
+ class InputError(Exception):
300
+ """Exception raised for errors in the Input module."""
301
+
302
+ def __init__(self, message: str) -> None:
303
+ self.message = message
304
+ super().__init__(self.message)
305
+
306
+
307
+ def _validate_against_schema(value: str, schema: Any) -> Any:
308
+ """Validate and convert input value against a schema.
309
+
310
+ Args:
311
+ value: The input value as a string.
312
+ schema: The schema to validate against.
313
+
314
+ Returns:
315
+ The converted/validated value.
316
+
317
+ Raises:
318
+ InputError: If validation fails.
319
+ """
320
+ if schema is None:
321
+ return value
322
+
323
+ try:
324
+ # Handle basic types
325
+ if schema == str:
326
+ return value
327
+ elif schema == int:
328
+ return int(value)
329
+ elif schema == float:
330
+ return float(value)
331
+ elif schema == bool:
332
+ return value.lower() in ("true", "t", "yes", "y", "1", "on")
333
+
334
+ # Handle dict - expect JSON input
335
+ elif schema == dict or (
336
+ hasattr(schema, "__origin__") and schema.__origin__ is dict
337
+ ):
338
+ try:
339
+ return json.loads(value)
340
+ except json.JSONDecodeError:
341
+ raise InputError(f"Invalid JSON format for dictionary input")
342
+
343
+ # Handle list - expect JSON input
344
+ elif schema == list or (
345
+ hasattr(schema, "__origin__") and schema.__origin__ is list
346
+ ):
347
+ try:
348
+ result = json.loads(value)
349
+ if not isinstance(result, list):
350
+ raise InputError("Expected a list")
351
+ return result
352
+ except json.JSONDecodeError:
353
+ raise InputError(f"Invalid JSON format for list input")
354
+
355
+ # Handle Union types (including Optional)
356
+ elif hasattr(schema, "__origin__") and schema.__origin__ is Union:
357
+ args = schema.__args__
358
+ if len(args) == 2 and type(None) in args:
359
+ # This is Optional[T]
360
+ if not value or value.lower() == "none":
361
+ return None
362
+ non_none_type = args[0] if args[1] is type(None) else args[1]
363
+ return _validate_against_schema(value, non_none_type)
364
+
365
+ # Handle Pydantic models
366
+ elif hasattr(schema, "model_validate_json"):
367
+ try:
368
+ return schema.model_validate_json(value)
369
+ except Exception as e:
370
+ raise InputError(f"Invalid input for {schema.__name__}: {e}")
371
+
372
+ # Handle BasedModels
373
+ elif hasattr(schema, "model_validate_json") or (
374
+ hasattr(schema, "__bases__")
375
+ and any("BasedModel" in str(base) for base in schema.__bases__)
376
+ ):
377
+ try:
378
+ return schema.model_validate_json(value)
379
+ except Exception as e:
380
+ raise InputError(f"Invalid input for {schema.__name__}: {e}")
381
+
382
+ # Handle dataclasses
383
+ elif hasattr(schema, "__dataclass_fields__"):
384
+ try:
385
+ data = json.loads(value)
386
+ return schema(**data)
387
+ except Exception as e:
388
+ raise InputError(f"Invalid input for {schema.__name__}: {e}")
389
+
390
+ # Handle TypedDict
391
+ elif hasattr(schema, "__annotations__") and hasattr(schema, "__total__"):
392
+ try:
393
+ return json.loads(value)
394
+ except json.JSONDecodeError:
395
+ raise InputError(f"Invalid JSON format for {schema.__name__}")
396
+
397
+ # Fallback - try to parse as JSON
398
+ else:
399
+ try:
400
+ return json.loads(value)
401
+ except json.JSONDecodeError:
402
+ return value
403
+
404
+ except InputError:
405
+ raise
406
+ except Exception as e:
407
+ raise InputError(f"Validation error: {e}")
408
+
409
+
410
+ def _collect_fields_sequentially(schema: Any, console) -> Dict[str, Any]:
411
+ """Collect field values sequentially for structured schemas.
412
+
413
+ Args:
414
+ schema: The schema to collect fields for.
415
+ console: The console to use for output.
416
+
417
+ Returns:
418
+ Dictionary of field names to values.
419
+ """
420
+ result = {}
421
+
422
+ try:
423
+ # Handle Pydantic models
424
+ if hasattr(schema, "model_fields"):
425
+ fields_info = schema.model_fields
426
+ console.print(
427
+ f"\n[bold blue]Entering data for {schema.__name__}:[/bold blue]"
428
+ )
429
+
430
+ for field_name, field_info in fields_info.items():
431
+ field_type = (
432
+ field_info.annotation if hasattr(field_info, "annotation") else str
433
+ )
434
+ default = getattr(field_info, "default", None)
435
+
436
+ prompt_text = f" {field_name}"
437
+ if default is not None and default != "...":
438
+ prompt_text += f" (default: {default})"
439
+ prompt_text += ": "
440
+
441
+ Prompt, _ = _get_rich_prompts()
442
+ value = Prompt.ask(prompt_text)
443
+ if not value and default is not None and default != "...":
444
+ result[field_name] = default
445
+ else:
446
+ try:
447
+ result[field_name] = _validate_against_schema(value, field_type)
448
+ except InputError as e:
449
+ console.print(f"[red]Error: {e}[/red]")
450
+ result[field_name] = value
451
+
452
+ # Handle BasedModels
453
+ elif hasattr(schema, "_get_fields_info"):
454
+ fields_info = schema._get_fields_info()
455
+ console.print(
456
+ f"\n[bold blue]Entering data for {schema.__name__}:[/bold blue]"
457
+ )
458
+
459
+ for field_name, field_info in fields_info.items():
460
+ field_type = field_info.get("type", str)
461
+ default = field_info.get("default")
462
+ required = field_info.get("required", True)
463
+
464
+ prompt_text = f" {field_name}"
465
+ if not required and default is not None:
466
+ prompt_text += f" (default: {default})"
467
+ elif not required:
468
+ prompt_text += " (optional)"
469
+ prompt_text += ": "
470
+
471
+ Prompt, _ = _get_rich_prompts()
472
+ value = Prompt.ask(prompt_text)
473
+ if not value and not required and default is not None:
474
+ result[field_name] = default
475
+ elif not value and not required:
476
+ continue
477
+ else:
478
+ try:
479
+ result[field_name] = _validate_against_schema(value, field_type)
480
+ except InputError as e:
481
+ console.print(f"[red]Error: {e}[/red]")
482
+ result[field_name] = value
483
+
484
+ # Handle dataclasses
485
+ elif hasattr(schema, "__dataclass_fields__"):
486
+ from ..typing import get_type_description
487
+ import dataclasses
488
+
489
+ fields_info = schema.__dataclass_fields__
490
+ console.print(
491
+ f"\n[bold blue]Entering data for {schema.__name__}:[/bold blue]"
492
+ )
493
+
494
+ for field_name, field_info in fields_info.items():
495
+ field_type = field_info.type
496
+ default = getattr(field_info, "default", None)
497
+
498
+ prompt_text = f" {field_name}"
499
+ if default is not None and default is not dataclasses.MISSING:
500
+ prompt_text += f" (default: {default})"
501
+ elif hasattr(field_type, "__name__"):
502
+ prompt_text += f" ({field_type.__name__})"
503
+ else:
504
+ prompt_text += f" ({get_type_description(field_type)})"
505
+ prompt_text += ""
506
+
507
+ Prompt, _ = _get_rich_prompts()
508
+ value = Prompt.ask(prompt_text)
509
+ if (
510
+ not value
511
+ and default is not None
512
+ and default is not dataclasses.MISSING
513
+ ):
514
+ result[field_name] = default
515
+ else:
516
+ try:
517
+ result[field_name] = _validate_against_schema(value, field_type)
518
+ except InputError as e:
519
+ console.print(f"[red]Error: {e}[/red]")
520
+ result[field_name] = value
521
+
522
+ # Handle TypedDict
523
+ elif hasattr(schema, "__annotations__"):
524
+ annotations = getattr(schema, "__annotations__", {})
525
+ console.print(
526
+ f"\n[bold blue]Entering data for {schema.__name__}:[/bold blue]"
527
+ )
528
+
529
+ for field_name, field_type in annotations.items():
530
+ prompt_text = f" {field_name}: "
531
+ Prompt, _ = _get_rich_prompts()
532
+ value = Prompt.ask(prompt_text)
533
+
534
+ if value:
535
+ try:
536
+ result[field_name] = _validate_against_schema(value, field_type)
537
+ except InputError as e:
538
+ console.print(f"[red]Error: {e}[/red]")
539
+ result[field_name] = value
540
+
541
+ except Exception as e:
542
+ console.print(f"[red]Error collecting fields: {e}[/red]")
543
+
544
+ return result
545
+
546
+
547
+ @overload
548
+ def input(
549
+ prompt: str = "",
550
+ schema: Any = None,
551
+ sequential: bool = True,
552
+ style: "CLIStyleType | None" = None,
553
+ style_settings: "CLIStyleRenderableSettings | None" = None,
554
+ bg: "CLIStyleBackgroundType | None" = None,
555
+ bg_settings: "CLIStyleBackgroundSettings | None" = None,
556
+ multiline: bool = False,
557
+ password: bool = False,
558
+ complete: Optional[List[str]] = None,
559
+ validate: Optional[callable] = None,
560
+ ) -> Any: ...
561
+
562
+
563
+ def input(
564
+ prompt: str = "",
565
+ schema: Any = None,
566
+ sequential: bool = True,
567
+ style: "CLIStyleType | None" = None,
568
+ style_settings: "CLIStyleRenderableSettings | None" = None,
569
+ bg: "CLIStyleBackgroundType | None" = None,
570
+ bg_settings: "CLIStyleBackgroundSettings | None" = None,
571
+ multiline: bool = False,
572
+ password: bool = False,
573
+ complete: Optional[List[str]] = None,
574
+ validate: Optional[callable] = None,
575
+ ) -> Any:
576
+ """
577
+ Stylized input function built with `rich` and `prompt_toolkit`. This method maintains
578
+ compatibility with the standard input function while adding advanced features like
579
+ schema validation, styling, and structured data input.
580
+
581
+ Args:
582
+ prompt: The prompt message to display.
583
+ schema: A type, model class, or schema to validate against. Supports:
584
+ - Basic types (str, int, float, bool)
585
+ - Collections (dict, list)
586
+ - Pydantic models
587
+ - BasedModels
588
+ - Dataclasses
589
+ - TypedDict
590
+ sequential: For schemas with multiple fields, request one field at a time.
591
+ style: A color or dictionary of style settings to apply to the prompt.
592
+ bg: A color or dictionary of background settings to apply to the prompt.
593
+ multiline: Whether to allow multiline input.
594
+ password: Whether to hide the input (password mode).
595
+ complete: List of completion options.
596
+ validate: Custom validation function.
597
+
598
+ Returns:
599
+ The validated input value, converted to the appropriate type based on schema.
600
+
601
+ Raises:
602
+ InputError: If validation fails or input is invalid.
603
+ """
604
+ get_console = _get_rich_console()
605
+ console = get_console()
606
+
607
+ try:
608
+ # If no special features are requested, use built-in input for compatibility
609
+ if (
610
+ schema is None
611
+ and style is None
612
+ and style_settings is None
613
+ and bg is None
614
+ and bg_settings is None
615
+ and not multiline
616
+ and not password
617
+ and complete is None
618
+ and validate is None
619
+ ):
620
+ return builtins.input(prompt)
621
+
622
+ # Apply styling to prompt if provided
623
+ _, style_renderable = _get_style_utils()
624
+ styled_prompt = style_renderable(
625
+ prompt,
626
+ style=style,
627
+ style_settings=style_settings,
628
+ bg=bg,
629
+ bg_settings=bg_settings,
630
+ )
631
+
632
+ # Handle schema-based input
633
+ if schema is not None:
634
+ # Handle bool schema with Confirm.ask
635
+ if schema == bool:
636
+ Prompt, Confirm = _get_rich_prompts()
637
+ return Confirm.ask(styled_prompt)
638
+
639
+ # Handle structured schemas with multiple fields
640
+ if sequential and (
641
+ hasattr(schema, "__annotations__")
642
+ or hasattr(schema, "model_fields")
643
+ or hasattr(schema, "_get_fields_info")
644
+ or hasattr(schema, "__dataclass_fields__")
645
+ ):
646
+ field_data = _collect_fields_sequentially(schema, console)
647
+
648
+ try:
649
+ # Create instance from collected data
650
+ if hasattr(schema, "model_validate"):
651
+ # Pydantic model
652
+ return schema.model_validate(field_data)
653
+ elif hasattr(schema, "__call__"):
654
+ # BasedModel, dataclass, or other callable
655
+ return schema(**field_data)
656
+ else:
657
+ # TypedDict or similar - return the dict
658
+ return field_data
659
+ except Exception as e:
660
+ console.print(f"[red]Error creating {schema.__name__}: {e}[/red]")
661
+ return field_data
662
+
663
+ # Handle single value input
664
+ Prompt, Confirm = _get_rich_prompts()
665
+ if password:
666
+ value = Prompt.ask(styled_prompt, password=True)
667
+ elif complete:
668
+ # Use prompt_toolkit for completion
669
+ pt_prompt, WordCompleter = _get_prompt_toolkit()
670
+ completer = WordCompleter(complete)
671
+ value = pt_prompt(str(styled_prompt), completer=completer)
672
+ elif multiline:
673
+ console.print(styled_prompt, end="")
674
+ lines = []
675
+ console.print("[dim](Enter empty line to finish)[/dim]")
676
+ pt_prompt, _ = _get_prompt_toolkit()
677
+ while True:
678
+ line = pt_prompt("... ")
679
+ if not line:
680
+ break
681
+ lines.append(line)
682
+ value = "\n".join(lines)
683
+ else:
684
+ # Regular input with Rich prompt
685
+ value = Prompt.ask(styled_prompt)
686
+
687
+ # Apply custom validation
688
+ if validate:
689
+ try:
690
+ if not validate(value):
691
+ raise InputError("Custom validation failed")
692
+ except Exception as e:
693
+ raise InputError(f"Validation error: {e}")
694
+
695
+ # Apply schema validation
696
+ if schema is not None:
697
+ return _validate_against_schema(value, schema)
698
+
699
+ return value
700
+
701
+ except KeyboardInterrupt:
702
+ console.print("\n[yellow]Input cancelled by user[/yellow]")
703
+ raise
704
+ except InputError:
705
+ raise
706
+ except Exception as e:
707
+ raise InputError(f"Input error: {e}")
708
+
709
+
710
+ def animate(
711
+ renderable: "RenderableType | str",
712
+ type: Literal["flashing", "pulsing", "shaking", "typing", "spinning", "rainbow"],
713
+ duration: Optional[float] = None,
714
+ # Animation parameters (defaults are handled by the specific animation classes)
715
+ speed: Optional[float] = None,
716
+ colors: "Optional[List[CLIStyleColorName]]" = None,
717
+ on_color: "Optional[CLIStyleColorName]" = None,
718
+ off_color: "Optional[CLIStyleColorName]" = None,
719
+ min_opacity: Optional[float] = None,
720
+ max_opacity: Optional[float] = None,
721
+ color: "Optional[CLIStyleColorName]" = None,
722
+ intensity: Optional[int] = None,
723
+ typing_speed: Optional[float] = None,
724
+ cursor: Optional[str] = None,
725
+ show_cursor: Optional[bool] = None,
726
+ frames: Optional[List[str]] = None,
727
+ prefix: Optional[bool] = None,
728
+ # Rich.Live parameters
729
+ refresh_rate: int = 20,
730
+ transient: bool = True,
731
+ auto_refresh: bool = True,
732
+ console: Optional["Console"] = None,
733
+ screen: bool = False,
734
+ vertical_overflow: str = "ellipsis",
735
+ ) -> None:
736
+ """Create and run an animation based on the specified type.
737
+
738
+ Args:
739
+ renderable: The object to animate (text, panel, etc.)
740
+ type: The type of animation to create
741
+ duration: Duration of the animation in seconds (defaults to 2.0)
742
+ speed: Animation speed (defaults to the specific animation class's default)
743
+ colors: Color list (used by flashing, rainbow) (defaults to the specific animation class's default)
744
+ on_color: Color when flashing "on" (used by flashing) (defaults to the specific animation class's default)
745
+ off_color: Color when flashing "off" (used by flashing) (defaults to the specific animation class's default)
746
+ min_opacity: Minimum opacity for pulsing animation (defaults to the specific animation class's default)
747
+ max_opacity: Maximum opacity for pulsing animation (defaults to the specific animation class's default)
748
+ color: Color for pulsing animation (defaults to the specific animation class's default)
749
+ intensity: Shaking intensity for shaking animation (defaults to the specific animation class's default)
750
+ typing_speed: Speed for typing animation (used by typing) (defaults to the specific animation class's default)
751
+ cursor: Cursor character for typing animation (used by typing) (defaults to the specific animation class's default)
752
+ show_cursor: Whether to show cursor for typing animation (used by typing) (defaults to the specific animation class's default)
753
+ frames: Custom frames for spinning animation (defaults to the specific animation class's default)
754
+ prefix: Whether to show spinner as prefix for spinning animation (defaults to the specific animation class's default)
755
+ refresh_rate: Refresh rate per second for Live rendering
756
+ transient: Whether to clear animation after completion
757
+ auto_refresh: Whether to auto-refresh the display
758
+ console: Console to use for rendering
759
+ screen: Whether to use alternate screen buffer
760
+ vertical_overflow: How to handle vertical overflow
761
+
762
+ Examples:
763
+ >>> animate("Hello!", type="flashing", duration=3.0, speed=0.3)
764
+ >>> animate(Panel("Loading"), type="pulsing", min_opacity=0.1)
765
+ >>> animate("Hello World!", type="typing", typing_speed=0.1)
766
+ >>> animate("Colorful!", type="rainbow", colors=["red", "blue"])
767
+ """
768
+ animations = _get_animation_classes()
769
+
770
+ if type == "flashing":
771
+ animation = animations["CLIFlashingAnimation"](
772
+ renderable,
773
+ speed=speed if speed is not None else 0.5,
774
+ colors=colors, # Class handles default if None
775
+ on_color=on_color if on_color is not None else "white",
776
+ off_color=off_color if off_color is not None else "dim white",
777
+ duration=duration, # Base class handles default if None
778
+ )
779
+ elif type == "pulsing":
780
+ animation = animations["CLIPulsingAnimation"](
781
+ renderable,
782
+ speed=speed if speed is not None else 2.0,
783
+ min_opacity=min_opacity if min_opacity is not None else 0.3,
784
+ max_opacity=max_opacity if max_opacity is not None else 1.0,
785
+ color=color if color is not None else "white",
786
+ duration=duration, # Base class handles default if None
787
+ )
788
+ elif type == "shaking":
789
+ animation = animations["CLIShakingAnimation"](
790
+ renderable,
791
+ intensity=intensity if intensity is not None else 1,
792
+ speed=speed if speed is not None else 0.1,
793
+ duration=duration, # Base class handles default if None
794
+ )
795
+ elif type == "typing":
796
+ # Note: CLITypingAnimation expects 'text', assuming renderable is a string here.
797
+ animation = animations["CLITypingAnimation"](
798
+ renderable,
799
+ speed=speed if speed is not None else 0.05, # Pass animate's speed, using CLITypingAnimation's speed default
800
+ typing_speed=typing_speed, # Pass animate's typing_speed, CLITypingAnimation handles its None default
801
+ cursor=cursor if cursor is not None else "█",
802
+ show_cursor=show_cursor if show_cursor is not None else True,
803
+ duration=duration, # Base class handles default if None
804
+ )
805
+ elif type == "spinning":
806
+ animation = animations["CLISpinningAnimation"](
807
+ renderable,
808
+ frames=frames, # Class handles default if None
809
+ speed=speed if speed is not None else 0.1,
810
+ prefix=prefix if prefix is not None else True,
811
+ duration=duration, # Base class handles default if None
812
+ )
813
+ elif type == "rainbow":
814
+ animation = animations["CLIRainbowAnimation"](
815
+ renderable,
816
+ speed=speed if speed is not None else 0.5,
817
+ colors=colors, # Class handles default if None
818
+ duration=duration, # Base class handles default if None
819
+ )
820
+ else:
821
+ raise ValueError(f"Unknown animation type: {type}")
822
+
823
+ # The animation object's animate method handles its own duration default
824
+ # and the Live parameters are passed directly from the function args
825
+ animation.animate(
826
+ duration=duration,
827
+ refresh_rate=refresh_rate,
828
+ transient=transient,
829
+ auto_refresh=auto_refresh,
830
+ console=console,
831
+ screen=screen,
832
+ vertical_overflow=vertical_overflow,
833
+ )
834
+
835
+
836
+ __all__ = ("print", "input", "animate")