hammad-python 0.0.10__py3-none-any.whl → 0.0.11__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.
- hammad/__init__.py +64 -10
- hammad/based/__init__.py +52 -0
- hammad/based/fields.py +546 -0
- hammad/based/model.py +968 -0
- hammad/based/utils.py +455 -0
- hammad/cache/__init__.py +30 -0
- hammad/{cache.py → cache/_cache.py} +83 -12
- hammad/cli/__init__.py +25 -0
- hammad/cli/plugins/__init__.py +786 -0
- hammad/cli/styles/__init__.py +5 -0
- hammad/cli/styles/animations.py +548 -0
- hammad/cli/styles/settings.py +135 -0
- hammad/cli/styles/types.py +358 -0
- hammad/cli/styles/utils.py +480 -0
- hammad/data/__init__.py +51 -0
- hammad/data/collections/__init__.py +32 -0
- hammad/data/collections/base_collection.py +58 -0
- hammad/data/collections/collection.py +227 -0
- hammad/data/collections/searchable_collection.py +556 -0
- hammad/data/collections/vector_collection.py +497 -0
- hammad/data/databases/__init__.py +21 -0
- hammad/data/databases/database.py +551 -0
- hammad/data/types/__init__.py +33 -0
- hammad/data/types/files/__init__.py +1 -0
- hammad/data/types/files/audio.py +81 -0
- hammad/data/types/files/configuration.py +475 -0
- hammad/data/types/files/document.py +195 -0
- hammad/data/types/files/file.py +358 -0
- hammad/data/types/files/image.py +80 -0
- hammad/json/__init__.py +21 -0
- hammad/{utils/json → json}/converters.py +4 -1
- hammad/logging/__init__.py +27 -0
- hammad/logging/decorators.py +432 -0
- hammad/logging/logger.py +534 -0
- hammad/pydantic/__init__.py +43 -0
- hammad/{utils/pydantic → pydantic}/converters.py +2 -1
- hammad/pydantic/models/__init__.py +28 -0
- hammad/pydantic/models/arbitrary_model.py +46 -0
- hammad/pydantic/models/cacheable_model.py +79 -0
- hammad/pydantic/models/fast_model.py +318 -0
- hammad/pydantic/models/function_model.py +176 -0
- hammad/pydantic/models/subscriptable_model.py +63 -0
- hammad/text/__init__.py +37 -0
- hammad/text/text.py +1068 -0
- hammad/text/utils/__init__.py +1 -0
- hammad/{utils/text → text/utils}/converters.py +2 -2
- hammad/text/utils/markdown/__init__.py +1 -0
- hammad/{utils → text/utils}/markdown/converters.py +3 -3
- hammad/{utils → text/utils}/markdown/formatting.py +1 -1
- hammad/{utils/typing/utils.py → typing/__init__.py} +75 -2
- hammad/web/__init__.py +42 -0
- hammad/web/http/__init__.py +1 -0
- hammad/web/http/client.py +944 -0
- hammad/web/openapi/client.py +740 -0
- hammad/web/search/__init__.py +1 -0
- hammad/web/search/client.py +936 -0
- hammad/web/utils.py +463 -0
- hammad/yaml/__init__.py +30 -0
- hammad/yaml/converters.py +19 -0
- {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/METADATA +14 -8
- hammad_python-0.0.11.dist-info/RECORD +65 -0
- hammad/database.py +0 -447
- hammad/logger.py +0 -273
- hammad/types/color.py +0 -951
- hammad/utils/json/__init__.py +0 -0
- hammad/utils/markdown/__init__.py +0 -0
- hammad/utils/pydantic/__init__.py +0 -0
- hammad/utils/text/__init__.py +0 -0
- hammad/utils/typing/__init__.py +0 -0
- hammad_python-0.0.10.dist-info/RECORD +0 -22
- /hammad/{types/__init__.py → py.typed} +0 -0
- /hammad/{utils → web/openapi}/__init__.py +0 -0
- {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/WHEEL +0 -0
- {hammad_python-0.0.10.dist-info → hammad_python-0.0.11.dist-info}/licenses/LICENSE +0 -0
hammad/based/utils.py
ADDED
@@ -0,0 +1,455 @@
|
|
1
|
+
"""hammad.based.utils"""
|
2
|
+
|
3
|
+
from functools import lru_cache
|
4
|
+
from typing import Any, Callable, Optional, Pattern, Union, Tuple, Dict, TYPE_CHECKING
|
5
|
+
import ast
|
6
|
+
import inspect
|
7
|
+
|
8
|
+
from msgspec.structs import Struct
|
9
|
+
|
10
|
+
from .fields import BasedFieldInfo, basedfield, BasedField
|
11
|
+
from .model import BasedModel
|
12
|
+
|
13
|
+
__all__ = (
|
14
|
+
"create_basedmodel",
|
15
|
+
"get_field_info",
|
16
|
+
"is_basedfield",
|
17
|
+
"is_basedmodel",
|
18
|
+
"based_validator",
|
19
|
+
"create_lazy_loader",
|
20
|
+
"auto_create_lazy_loader",
|
21
|
+
"install",
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
def create_basedmodel(
|
26
|
+
__model_name: str,
|
27
|
+
*,
|
28
|
+
__base__: Optional[Union[type, Tuple[type, ...]]] = None,
|
29
|
+
__module__: Optional[str] = None,
|
30
|
+
__qualname__: Optional[str] = None,
|
31
|
+
__doc__: Optional[str] = None,
|
32
|
+
__validators__: Optional[Dict[str, Any]] = None,
|
33
|
+
__config__: Optional[type] = None,
|
34
|
+
**field_definitions: Any,
|
35
|
+
) -> type[BasedModel]:
|
36
|
+
"""Create a BasedModel dynamically with Pydantic-compatible interface.
|
37
|
+
|
38
|
+
This function provides a drop-in replacement for pydantic.create_model()
|
39
|
+
that creates BasedModel classes instead of pydantic BaseModel classes.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
__model_name: Name of the model class to create
|
43
|
+
__base__: Base class(es) to inherit from. If None, uses BasedModel.
|
44
|
+
Can be a single class or tuple of classes.
|
45
|
+
__module__: Module name for the created class
|
46
|
+
__qualname__: Qualified name for the created class
|
47
|
+
__doc__: Docstring for the created class
|
48
|
+
__validators__: Dictionary of validators (for compatibility - not fully implemented)
|
49
|
+
__config__: Configuration class (for compatibility - not fully implemented)
|
50
|
+
**field_definitions: Field definitions as keyword arguments.
|
51
|
+
Each can be:
|
52
|
+
- A type annotation (e.g., str, int)
|
53
|
+
- A tuple of (type, default_value)
|
54
|
+
- A tuple of (type, Field(...))
|
55
|
+
- A Field instance
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
A new BasedModel class with the specified fields
|
59
|
+
|
60
|
+
Examples:
|
61
|
+
# Simple model with basic types
|
62
|
+
User = create_basedmodel('User', name=str, age=int)
|
63
|
+
|
64
|
+
# Model with defaults
|
65
|
+
Config = create_basedmodel('Config',
|
66
|
+
host=(str, 'localhost'),
|
67
|
+
port=(int, 8080))
|
68
|
+
|
69
|
+
# Model with field constraints
|
70
|
+
Product = create_basedmodel('Product',
|
71
|
+
name=str,
|
72
|
+
price=(float, basedfield(gt=0)),
|
73
|
+
tags=(List[str], basedfield(default_factory=list)))
|
74
|
+
|
75
|
+
# Model with custom base class
|
76
|
+
class BaseEntity(BasedModel):
|
77
|
+
id: int
|
78
|
+
created_at: str
|
79
|
+
|
80
|
+
User = create_basedmodel('User',
|
81
|
+
name=str,
|
82
|
+
email=str,
|
83
|
+
__base__=BaseEntity)
|
84
|
+
"""
|
85
|
+
# Handle base class specification
|
86
|
+
if __base__ is not None and __config__ is not None:
|
87
|
+
raise ValueError(
|
88
|
+
"Cannot specify both '__base__' and '__config__' - "
|
89
|
+
"use a base class with the desired configuration instead"
|
90
|
+
)
|
91
|
+
|
92
|
+
# Determine base classes
|
93
|
+
if __base__ is None:
|
94
|
+
bases = (BasedModel,)
|
95
|
+
elif isinstance(__base__, tuple):
|
96
|
+
# Ensure all bases are compatible
|
97
|
+
for base in __base__:
|
98
|
+
if not (issubclass(base, BasedModel) or issubclass(base, Struct)):
|
99
|
+
raise ValueError(
|
100
|
+
f"Base class {base} must be a subclass of BasedModel or msgspec.Struct"
|
101
|
+
)
|
102
|
+
bases = __base__
|
103
|
+
else:
|
104
|
+
if not (issubclass(__base__, BasedModel) or issubclass(__base__, Struct)):
|
105
|
+
raise ValueError(
|
106
|
+
f"Base class {__base__} must be a subclass of BasedModel or msgspec.Struct"
|
107
|
+
)
|
108
|
+
bases = (__base__,)
|
109
|
+
|
110
|
+
# Build class dictionary
|
111
|
+
class_dict = {}
|
112
|
+
annotations = {}
|
113
|
+
|
114
|
+
# Set metadata
|
115
|
+
if __doc__ is not None:
|
116
|
+
class_dict["__doc__"] = __doc__
|
117
|
+
if __module__ is not None:
|
118
|
+
class_dict["__module__"] = __module__
|
119
|
+
if __qualname__ is not None:
|
120
|
+
class_dict["__qualname__"] = __qualname__
|
121
|
+
|
122
|
+
# Process field definitions in two passes to ensure proper ordering
|
123
|
+
# First pass: collect required and optional fields separately
|
124
|
+
required_fields = {}
|
125
|
+
optional_fields = {}
|
126
|
+
|
127
|
+
for field_name, field_definition in field_definitions.items():
|
128
|
+
if field_name.startswith("__") and field_name.endswith("__"):
|
129
|
+
# Skip special attributes that were passed as field definitions
|
130
|
+
continue
|
131
|
+
|
132
|
+
# Parse field definition
|
133
|
+
is_optional = False
|
134
|
+
|
135
|
+
if isinstance(field_definition, tuple):
|
136
|
+
if len(field_definition) == 2:
|
137
|
+
field_type, field_value = field_definition
|
138
|
+
annotations[field_name] = field_type
|
139
|
+
|
140
|
+
# Check if field_value is a Field instance or basedfield
|
141
|
+
if hasattr(field_value, "__class__") and (
|
142
|
+
"field" in field_value.__class__.__name__.lower()
|
143
|
+
or hasattr(field_value, "default")
|
144
|
+
or callable(getattr(field_value, "__call__", None))
|
145
|
+
):
|
146
|
+
# It's a field descriptor
|
147
|
+
optional_fields[field_name] = field_value
|
148
|
+
else:
|
149
|
+
# It's a default value - create a basedfield with this default
|
150
|
+
optional_fields[field_name] = basedfield(default=field_value)
|
151
|
+
is_optional = True
|
152
|
+
else:
|
153
|
+
raise ValueError(
|
154
|
+
f"Field definition for '{field_name}' must be a 2-tuple of (type, default/Field)"
|
155
|
+
)
|
156
|
+
elif hasattr(field_definition, "__origin__") or hasattr(
|
157
|
+
field_definition, "__class__"
|
158
|
+
):
|
159
|
+
# It's a type annotation (like str, int, List[str], etc.) - required field
|
160
|
+
annotations[field_name] = field_definition
|
161
|
+
required_fields[field_name] = None
|
162
|
+
else:
|
163
|
+
# It's likely a default value without type annotation
|
164
|
+
# We'll infer the type from the value
|
165
|
+
annotations[field_name] = type(field_definition)
|
166
|
+
optional_fields[field_name] = basedfield(default=field_definition)
|
167
|
+
is_optional = True
|
168
|
+
|
169
|
+
# Second pass: add fields in correct order (required first, then optional)
|
170
|
+
# This ensures msgspec field ordering requirements are met
|
171
|
+
for field_name, field_value in required_fields.items():
|
172
|
+
if field_value is not None:
|
173
|
+
class_dict[field_name] = field_value
|
174
|
+
|
175
|
+
for field_name, field_value in optional_fields.items():
|
176
|
+
class_dict[field_name] = field_value
|
177
|
+
|
178
|
+
# Set annotations in proper order (required fields first, then optional)
|
179
|
+
ordered_annotations = {}
|
180
|
+
|
181
|
+
# Add required field annotations first
|
182
|
+
for field_name in required_fields:
|
183
|
+
if field_name in annotations:
|
184
|
+
ordered_annotations[field_name] = annotations[field_name]
|
185
|
+
|
186
|
+
# Add optional field annotations second
|
187
|
+
for field_name in optional_fields:
|
188
|
+
if field_name in annotations:
|
189
|
+
ordered_annotations[field_name] = annotations[field_name]
|
190
|
+
|
191
|
+
class_dict["__annotations__"] = ordered_annotations
|
192
|
+
|
193
|
+
# Handle validators (basic implementation for compatibility)
|
194
|
+
if __validators__:
|
195
|
+
# Store validators for potential future use
|
196
|
+
class_dict["_based_validators"] = __validators__
|
197
|
+
# Note: Full validator implementation would require more complex integration
|
198
|
+
|
199
|
+
# Create the dynamic class
|
200
|
+
try:
|
201
|
+
DynamicModel = type(__model_name, bases, class_dict)
|
202
|
+
except Exception as e:
|
203
|
+
raise ValueError(f"Failed to create model '{__model_name}': {e}") from e
|
204
|
+
|
205
|
+
return DynamicModel
|
206
|
+
|
207
|
+
|
208
|
+
@lru_cache(maxsize=None)
|
209
|
+
def get_field_info(field: Any) -> Optional[BasedFieldInfo]:
|
210
|
+
"""Extract FieldInfo from a field descriptor with caching."""
|
211
|
+
if isinstance(field, tuple) and len(field) == 2:
|
212
|
+
_, field_info = field
|
213
|
+
if isinstance(field_info, BasedFieldInfo):
|
214
|
+
return field_info
|
215
|
+
elif hasattr(field, "_basedfield_info"):
|
216
|
+
return field._basedfield_info
|
217
|
+
elif hasattr(field, "field_info"):
|
218
|
+
return field.field_info
|
219
|
+
elif isinstance(field, BasedField):
|
220
|
+
return field.field_info
|
221
|
+
elif hasattr(field, "__class__") and field.__class__.__name__ == "FieldDescriptor":
|
222
|
+
return field.field_info
|
223
|
+
return None
|
224
|
+
|
225
|
+
|
226
|
+
def is_basedfield(field: Any) -> bool:
|
227
|
+
"""Check if a field is a basedfield."""
|
228
|
+
return get_field_info(field) is not None
|
229
|
+
|
230
|
+
|
231
|
+
def is_basedmodel(model: Any) -> bool:
|
232
|
+
"""Check if a model is a basedmodel."""
|
233
|
+
# Check if it's an instance of BasedModel
|
234
|
+
if isinstance(model, BasedModel):
|
235
|
+
return True
|
236
|
+
|
237
|
+
# Check if it's a BasedModel class (not instance)
|
238
|
+
if isinstance(model, type) and issubclass(model, BasedModel):
|
239
|
+
return True
|
240
|
+
|
241
|
+
# Check for BasedModel characteristics using duck typing
|
242
|
+
# Look for key BasedModel/msgspec.Struct attributes and methods
|
243
|
+
if hasattr(model, "__struct_fields__") and hasattr(model, "model_dump"):
|
244
|
+
# Check for BasedModel-specific methods
|
245
|
+
if (
|
246
|
+
hasattr(model, "model_copy")
|
247
|
+
and hasattr(model, "model_validate")
|
248
|
+
and hasattr(model, "model_to_pydantic")
|
249
|
+
):
|
250
|
+
return True
|
251
|
+
|
252
|
+
# Check if it's an instance of any msgspec Struct with BasedModel methods
|
253
|
+
try:
|
254
|
+
if isinstance(model, Struct) and hasattr(model, "model_dump"):
|
255
|
+
return True
|
256
|
+
except ImportError:
|
257
|
+
pass
|
258
|
+
|
259
|
+
return False
|
260
|
+
|
261
|
+
|
262
|
+
def based_validator(
|
263
|
+
*fields: str, pre: bool = False, post: bool = False, always: bool = False
|
264
|
+
):
|
265
|
+
"""Decorator to create a validator for specific fields.
|
266
|
+
|
267
|
+
Args:
|
268
|
+
*fields: Field names to validate
|
269
|
+
pre: Whether this is a pre-validator
|
270
|
+
post: Whether this is a post-validator
|
271
|
+
always: Whether to run even if the value is not set
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
Decorator function
|
275
|
+
"""
|
276
|
+
|
277
|
+
def decorator(func: Callable) -> Callable:
|
278
|
+
func._validator_fields = fields
|
279
|
+
func._validator_pre = pre
|
280
|
+
func._validator_post = post
|
281
|
+
func._validator_always = always
|
282
|
+
return func
|
283
|
+
|
284
|
+
return decorator
|
285
|
+
|
286
|
+
|
287
|
+
def create_lazy_loader(
|
288
|
+
imports_dict: dict[str, str], package: str
|
289
|
+
) -> Callable[[str], Any]:
|
290
|
+
"""Create a lazy loader function for __getattr__.
|
291
|
+
|
292
|
+
Args:
|
293
|
+
imports_dict: Dictionary mapping attribute names to their module paths
|
294
|
+
package: The package name for import_module
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
A __getattr__ function that lazily imports modules
|
298
|
+
"""
|
299
|
+
|
300
|
+
def __getattr__(name: str) -> Any:
|
301
|
+
if name in imports_dict:
|
302
|
+
from importlib import import_module
|
303
|
+
|
304
|
+
module_path, original_name = imports_dict[name]
|
305
|
+
module = import_module(module_path, package)
|
306
|
+
return getattr(module, original_name)
|
307
|
+
raise AttributeError(f"module '{package}' has no attribute '{name}'")
|
308
|
+
|
309
|
+
return __getattr__
|
310
|
+
|
311
|
+
|
312
|
+
def auto_create_lazy_loader(all_exports: tuple[str, ...]) -> Callable[[str], Any]:
|
313
|
+
"""Automatically create a lazy loader by inspecting the calling module.
|
314
|
+
|
315
|
+
This function inspects the calling module's source code to extract
|
316
|
+
TYPE_CHECKING imports and automatically builds the import map.
|
317
|
+
|
318
|
+
Args:
|
319
|
+
all_exports: The __all__ tuple from the calling module
|
320
|
+
|
321
|
+
Returns:
|
322
|
+
A __getattr__ function that lazily imports modules
|
323
|
+
"""
|
324
|
+
# Get the calling module's frame
|
325
|
+
frame = inspect.currentframe()
|
326
|
+
if frame is None or frame.f_back is None:
|
327
|
+
raise RuntimeError("Cannot determine calling module")
|
328
|
+
|
329
|
+
calling_frame = frame.f_back
|
330
|
+
module_name = calling_frame.f_globals.get("__name__", "")
|
331
|
+
package = calling_frame.f_globals.get("__package__", "")
|
332
|
+
filename = calling_frame.f_globals.get("__file__", "")
|
333
|
+
|
334
|
+
# Read the source file
|
335
|
+
try:
|
336
|
+
with open(filename, "r") as f:
|
337
|
+
source_code = f.read()
|
338
|
+
except (IOError, OSError):
|
339
|
+
# Fallback: try to get source from the module
|
340
|
+
import sys
|
341
|
+
|
342
|
+
module = sys.modules.get(module_name)
|
343
|
+
if module:
|
344
|
+
source_code = inspect.getsource(module)
|
345
|
+
else:
|
346
|
+
raise RuntimeError(f"Cannot read source for module {module_name}")
|
347
|
+
|
348
|
+
# Parse the source to extract TYPE_CHECKING imports
|
349
|
+
imports_map = _parse_type_checking_imports(source_code)
|
350
|
+
|
351
|
+
# Filter to only include exports that are in __all__
|
352
|
+
filtered_map = {
|
353
|
+
name: path for name, path in imports_map.items() if name in all_exports
|
354
|
+
}
|
355
|
+
|
356
|
+
return create_lazy_loader(filtered_map, package)
|
357
|
+
|
358
|
+
|
359
|
+
def _parse_type_checking_imports(source_code: str) -> dict[str, tuple[str, str]]:
|
360
|
+
"""Parse TYPE_CHECKING imports from source code to build import map.
|
361
|
+
|
362
|
+
Args:
|
363
|
+
source_code: The source code containing TYPE_CHECKING imports
|
364
|
+
|
365
|
+
Returns:
|
366
|
+
Dictionary mapping local names to (module_path, original_name) tuples
|
367
|
+
"""
|
368
|
+
tree = ast.parse(source_code)
|
369
|
+
imports_map = {}
|
370
|
+
|
371
|
+
class TypeCheckingVisitor(ast.NodeVisitor):
|
372
|
+
def __init__(self):
|
373
|
+
self.in_type_checking = False
|
374
|
+
self.imports = {}
|
375
|
+
|
376
|
+
def visit_If(self, node):
|
377
|
+
# Check if this is a TYPE_CHECKING block
|
378
|
+
is_type_checking = False
|
379
|
+
|
380
|
+
if isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING":
|
381
|
+
is_type_checking = True
|
382
|
+
elif isinstance(node.test, ast.Attribute):
|
383
|
+
if (
|
384
|
+
isinstance(node.test.value, ast.Name)
|
385
|
+
and node.test.value.id == "typing"
|
386
|
+
and node.test.attr == "TYPE_CHECKING"
|
387
|
+
):
|
388
|
+
is_type_checking = True
|
389
|
+
|
390
|
+
if is_type_checking:
|
391
|
+
old_state = self.in_type_checking
|
392
|
+
self.in_type_checking = True
|
393
|
+
for stmt in node.body:
|
394
|
+
self.visit(stmt)
|
395
|
+
self.in_type_checking = old_state
|
396
|
+
else:
|
397
|
+
self.generic_visit(node)
|
398
|
+
|
399
|
+
def visit_ImportFrom(self, node):
|
400
|
+
if self.in_type_checking and node.module:
|
401
|
+
module_path = f".{node.module}"
|
402
|
+
for alias in node.names:
|
403
|
+
original_name = alias.name
|
404
|
+
local_name = alias.asname or original_name
|
405
|
+
self.imports[local_name] = (module_path, original_name)
|
406
|
+
self.generic_visit(node)
|
407
|
+
|
408
|
+
visitor = TypeCheckingVisitor()
|
409
|
+
visitor.visit(tree)
|
410
|
+
|
411
|
+
return visitor.imports
|
412
|
+
|
413
|
+
|
414
|
+
def install(
|
415
|
+
traceback: bool = True,
|
416
|
+
uvloop: bool = True,
|
417
|
+
print: bool = False,
|
418
|
+
input: bool = False,
|
419
|
+
):
|
420
|
+
"""Installs various 'opinionated' and optional resources that help
|
421
|
+
enhance the development experience, or increase performance.
|
422
|
+
|
423
|
+
Args:
|
424
|
+
traceback (bool): Whether to install the rich traceback handler.
|
425
|
+
uvloop (bool): Whether to install uvloop.
|
426
|
+
print (bool): Whether to install the stylized `print` method as a builtin
|
427
|
+
from `hammad.cli`. NOTE: IDEs will not recognize this change statically.
|
428
|
+
input (bool): Whether to install the stylized `input` method as a builtin
|
429
|
+
from `hammad.cli`. NOTE: IDEs will not recognize this change statically.
|
430
|
+
|
431
|
+
Note:
|
432
|
+
IDE limitations: Static analysis tools cannot detect runtime builtin modifications.
|
433
|
+
The IDE will still show the original builtin definitions. Consider using explicit
|
434
|
+
imports for better IDE support:
|
435
|
+
|
436
|
+
from hammad.cli.plugins import print, input
|
437
|
+
"""
|
438
|
+
import builtins
|
439
|
+
|
440
|
+
if traceback:
|
441
|
+
from rich.traceback import install
|
442
|
+
|
443
|
+
install()
|
444
|
+
if uvloop:
|
445
|
+
from uvloop import install
|
446
|
+
|
447
|
+
install()
|
448
|
+
if print:
|
449
|
+
from hammad.cli.plugins import print as print_fn
|
450
|
+
|
451
|
+
builtins.print = print_fn
|
452
|
+
if input:
|
453
|
+
from hammad.cli.plugins import input as input_fn
|
454
|
+
|
455
|
+
builtins.input = input_fn
|
hammad/cache/__init__.py
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
"""hammad.cache"""
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
from ..based.utils import auto_create_lazy_loader
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from ._cache import (
|
8
|
+
TTLCache,
|
9
|
+
DiskCache,
|
10
|
+
CacheParams,
|
11
|
+
CacheReturn,
|
12
|
+
cached,
|
13
|
+
auto_cached,
|
14
|
+
create_cache,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
__all__ = (
|
19
|
+
"auto_cached",
|
20
|
+
"cached",
|
21
|
+
"create_cache",
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
__getattr__ = auto_create_lazy_loader(__all__)
|
26
|
+
|
27
|
+
|
28
|
+
def __dir__() -> list[str]:
|
29
|
+
"""Get the attributes of the cache module."""
|
30
|
+
return list(__all__)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
"""
|
1
|
+
"""hammad.cache._cache
|
2
2
|
|
3
3
|
Contains helpful resources for creating simple cache systems, and
|
4
4
|
decorators that implement "automatic" hashing & caching of function calls.
|
@@ -21,13 +21,13 @@ from typing import (
|
|
21
21
|
TypeVar,
|
22
22
|
Tuple,
|
23
23
|
Optional,
|
24
|
-
Protocol,
|
25
24
|
overload,
|
26
25
|
ParamSpec,
|
27
26
|
Literal,
|
28
27
|
get_args,
|
29
28
|
TypeAlias,
|
30
29
|
Union,
|
30
|
+
overload,
|
31
31
|
)
|
32
32
|
|
33
33
|
__all__ = [
|
@@ -39,6 +39,7 @@ __all__ = [
|
|
39
39
|
"CacheType",
|
40
40
|
"CacheParams",
|
41
41
|
"CacheReturn",
|
42
|
+
"create_cache",
|
42
43
|
]
|
43
44
|
|
44
45
|
|
@@ -47,7 +48,7 @@ __all__ = [
|
|
47
48
|
# -----------------------------------------------------------------------------
|
48
49
|
|
49
50
|
CacheType: TypeAlias = Literal["ttl", "disk"]
|
50
|
-
"""Type of caches that can be created using `
|
51
|
+
"""Type of caches that can be created using `hammad`.
|
51
52
|
|
52
53
|
- `"ttl"`: Time-to-live cache.
|
53
54
|
- `"disk"`: Disk-based cache.
|
@@ -65,16 +66,16 @@ CacheReturn = TypeVar("CacheReturn")
|
|
65
66
|
# -----------------------------------------------------------------------------
|
66
67
|
|
67
68
|
|
68
|
-
|
69
|
-
"""Internal cache for the `
|
69
|
+
_hammad_CACHE: None | BaseCache = None
|
70
|
+
"""Internal cache for the `hammad` package. Instantiated when needed."""
|
70
71
|
|
71
72
|
|
72
73
|
def _get_cache() -> BaseCache:
|
73
74
|
"""Returns the global cache instance, creating it if necessary."""
|
74
|
-
global
|
75
|
-
if
|
76
|
-
|
77
|
-
return
|
75
|
+
global _hammad_CACHE
|
76
|
+
if _hammad_CACHE is None:
|
77
|
+
_hammad_CACHE = TTLCache(maxsize=1000, ttl=3600)
|
78
|
+
return _hammad_CACHE
|
78
79
|
|
79
80
|
|
80
81
|
# -----------------------------------------------------------------------------
|
@@ -84,7 +85,7 @@ def _get_cache() -> BaseCache:
|
|
84
85
|
|
85
86
|
@dataclass
|
86
87
|
class BaseCache:
|
87
|
-
"""Base class for all caches created using `
|
88
|
+
"""Base class for all caches created using `hammad`."""
|
88
89
|
|
89
90
|
type: CacheType
|
90
91
|
"""Type of cache."""
|
@@ -491,7 +492,7 @@ def cached(
|
|
491
492
|
Flexible caching decorator that preserves type hints and signatures.
|
492
493
|
|
493
494
|
Can be used with or without arguments:
|
494
|
-
- `@cached`: Uses automatic key generation with the global `
|
495
|
+
- `@cached`: Uses automatic key generation with the global `hammad.cache.CACHE`.
|
495
496
|
- `@cached(key=custom_key_func)`: Uses a custom key generation function.
|
496
497
|
- `@cached(ttl=300, maxsize=50)`: Creates a new `TTLCache` instance specifically
|
497
498
|
for the decorated function with the given TTL and maxsize.
|
@@ -615,7 +616,7 @@ def auto_cached(
|
|
615
616
|
|
616
617
|
Example:
|
617
618
|
```python
|
618
|
-
from
|
619
|
+
from hammad.cache import auto_cached, create_cache
|
619
620
|
|
620
621
|
# Example of using a custom cache instance
|
621
622
|
my_user_cache = create_cache(cache_type="ttl", ttl=600, maxsize=50)
|
@@ -673,3 +674,73 @@ def auto_cached(
|
|
673
674
|
return configured_cached_decorator(func_to_decorate)
|
674
675
|
|
675
676
|
return actual_decorator
|
677
|
+
|
678
|
+
|
679
|
+
# -----------------------------------------------------------------------------
|
680
|
+
# CACHE FACTORY
|
681
|
+
# -----------------------------------------------------------------------------
|
682
|
+
|
683
|
+
|
684
|
+
@overload
|
685
|
+
def create_cache(
|
686
|
+
cache_type: Literal["ttl"], *, maxsize: int = 128, ttl: Optional[float] = None
|
687
|
+
) -> TTLCache: ...
|
688
|
+
|
689
|
+
|
690
|
+
@overload
|
691
|
+
def create_cache(
|
692
|
+
cache_type: Literal["disk"],
|
693
|
+
*,
|
694
|
+
cache_dir: Optional[Union[str, Path]] = None,
|
695
|
+
maxsize: int = 128,
|
696
|
+
) -> DiskCache: ...
|
697
|
+
|
698
|
+
|
699
|
+
@overload
|
700
|
+
def create_cache(cache_type: CacheType, **kwargs: Any) -> BaseCache: ...
|
701
|
+
|
702
|
+
|
703
|
+
def create_cache(cache_type: CacheType, **kwargs: Any) -> BaseCache:
|
704
|
+
"""
|
705
|
+
Factory function to create cache instances of different types.
|
706
|
+
|
707
|
+
Args:
|
708
|
+
cache_type: The type of cache to create. Can be "ttl" or "disk".
|
709
|
+
**kwargs: Additional keyword arguments specific to the cache type.
|
710
|
+
|
711
|
+
Returns:
|
712
|
+
A cache instance of the specified type.
|
713
|
+
|
714
|
+
Raises:
|
715
|
+
ValueError: If an unsupported cache type is provided.
|
716
|
+
|
717
|
+
Examples:
|
718
|
+
```python
|
719
|
+
# Create a TTL cache with custom settings
|
720
|
+
ttl_cache = create_cache("ttl", maxsize=256, ttl=300)
|
721
|
+
|
722
|
+
# Create a disk cache with custom directory
|
723
|
+
disk_cache = create_cache("disk", cache_dir="/tmp/my_cache", maxsize=1000)
|
724
|
+
```
|
725
|
+
"""
|
726
|
+
if cache_type == "ttl":
|
727
|
+
maxsize = kwargs.pop("maxsize", 128)
|
728
|
+
ttl = kwargs.pop("ttl", None)
|
729
|
+
if kwargs:
|
730
|
+
raise TypeError(
|
731
|
+
f"Unexpected keyword arguments for TTL cache: {list(kwargs.keys())}"
|
732
|
+
)
|
733
|
+
return TTLCache(maxsize=maxsize, ttl=ttl)
|
734
|
+
elif cache_type == "disk":
|
735
|
+
cache_dir = kwargs.pop("cache_dir", None)
|
736
|
+
maxsize = kwargs.pop("maxsize", 128)
|
737
|
+
if kwargs:
|
738
|
+
raise TypeError(
|
739
|
+
f"Unexpected keyword arguments for disk cache: {list(kwargs.keys())}"
|
740
|
+
)
|
741
|
+
return DiskCache(cache_dir=cache_dir, maxsize=maxsize)
|
742
|
+
else:
|
743
|
+
valid_types = get_args(CacheType)
|
744
|
+
raise ValueError(
|
745
|
+
f"Unsupported cache type: {cache_type}. Valid types are: {valid_types}"
|
746
|
+
)
|
hammad/cli/__init__.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
"""hammad.cli
|
2
|
+
|
3
|
+
Contains resources for styling rendered CLI content as well
|
4
|
+
as extensions / utilities for creating CLI interfaces."""
|
5
|
+
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
from ..based.utils import auto_create_lazy_loader
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from .plugins import print, input, animate
|
11
|
+
|
12
|
+
|
13
|
+
__all__ = (
|
14
|
+
"print",
|
15
|
+
"input",
|
16
|
+
"animate",
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
__getattr__ = auto_create_lazy_loader(__all__)
|
21
|
+
|
22
|
+
|
23
|
+
def __dir__() -> list[str]:
|
24
|
+
"""Get the attributes of the plugins module."""
|
25
|
+
return list(__all__)
|