hammad-python 0.0.15__py3-none-any.whl → 0.0.16__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 +178 -0
- hammad/_internal.py +237 -0
- hammad/cache/__init__.py +40 -0
- hammad/cache/base_cache.py +181 -0
- hammad/cache/cache.py +169 -0
- hammad/cache/decorators.py +261 -0
- hammad/cache/file_cache.py +80 -0
- hammad/cache/ttl_cache.py +74 -0
- hammad/cli/__init__.py +35 -0
- hammad/cli/_runner.py +265 -0
- hammad/cli/animations.py +573 -0
- hammad/cli/plugins.py +836 -0
- hammad/cli/styles/__init__.py +55 -0
- hammad/cli/styles/settings.py +139 -0
- hammad/cli/styles/types.py +358 -0
- hammad/cli/styles/utils.py +626 -0
- hammad/data/__init__.py +83 -0
- hammad/data/collections/__init__.py +44 -0
- hammad/data/collections/collection.py +274 -0
- hammad/data/collections/indexes/__init__.py +37 -0
- hammad/data/collections/indexes/qdrant/__init__.py +1 -0
- hammad/data/collections/indexes/qdrant/index.py +735 -0
- hammad/data/collections/indexes/qdrant/settings.py +94 -0
- hammad/data/collections/indexes/qdrant/utils.py +220 -0
- hammad/data/collections/indexes/tantivy/__init__.py +1 -0
- hammad/data/collections/indexes/tantivy/index.py +428 -0
- hammad/data/collections/indexes/tantivy/settings.py +51 -0
- hammad/data/collections/indexes/tantivy/utils.py +200 -0
- hammad/data/configurations/__init__.py +35 -0
- hammad/data/configurations/configuration.py +564 -0
- hammad/data/models/__init__.py +55 -0
- hammad/data/models/extensions/__init__.py +4 -0
- hammad/data/models/extensions/pydantic/__init__.py +42 -0
- hammad/data/models/extensions/pydantic/converters.py +759 -0
- hammad/data/models/fields.py +546 -0
- hammad/data/models/model.py +1078 -0
- hammad/data/models/utils.py +280 -0
- hammad/data/sql/__init__.py +23 -0
- hammad/data/sql/database.py +578 -0
- hammad/data/sql/types.py +141 -0
- hammad/data/types/__init__.py +39 -0
- hammad/data/types/file.py +358 -0
- hammad/data/types/multimodal/__init__.py +24 -0
- hammad/data/types/multimodal/audio.py +96 -0
- hammad/data/types/multimodal/image.py +80 -0
- hammad/data/types/text.py +1066 -0
- hammad/formatting/__init__.py +20 -0
- hammad/formatting/json/__init__.py +27 -0
- hammad/formatting/json/converters.py +158 -0
- hammad/formatting/text/__init__.py +63 -0
- hammad/formatting/text/converters.py +723 -0
- hammad/formatting/text/markdown.py +131 -0
- hammad/formatting/yaml/__init__.py +26 -0
- hammad/formatting/yaml/converters.py +5 -0
- hammad/genai/__init__.py +78 -0
- hammad/genai/agents/__init__.py +1 -0
- hammad/genai/agents/types/__init__.py +35 -0
- hammad/genai/agents/types/history.py +277 -0
- hammad/genai/agents/types/tool.py +490 -0
- hammad/genai/embedding_models/__init__.py +41 -0
- hammad/genai/embedding_models/embedding_model.py +193 -0
- hammad/genai/embedding_models/embedding_model_name.py +77 -0
- hammad/genai/embedding_models/embedding_model_request.py +65 -0
- hammad/genai/embedding_models/embedding_model_response.py +69 -0
- hammad/genai/embedding_models/run.py +161 -0
- hammad/genai/language_models/__init__.py +35 -0
- hammad/genai/language_models/_streaming.py +622 -0
- hammad/genai/language_models/_types.py +276 -0
- hammad/genai/language_models/_utils/__init__.py +31 -0
- hammad/genai/language_models/_utils/_completions.py +131 -0
- hammad/genai/language_models/_utils/_messages.py +89 -0
- hammad/genai/language_models/_utils/_requests.py +202 -0
- hammad/genai/language_models/_utils/_structured_outputs.py +124 -0
- hammad/genai/language_models/language_model.py +734 -0
- hammad/genai/language_models/language_model_request.py +135 -0
- hammad/genai/language_models/language_model_response.py +219 -0
- hammad/genai/language_models/language_model_response_chunk.py +53 -0
- hammad/genai/language_models/run.py +530 -0
- hammad/genai/multimodal_models.py +48 -0
- hammad/genai/rerank_models.py +26 -0
- hammad/logging/__init__.py +35 -0
- hammad/logging/decorators.py +834 -0
- hammad/logging/logger.py +954 -0
- hammad/mcp/__init__.py +50 -0
- hammad/mcp/client/__init__.py +36 -0
- hammad/mcp/client/client.py +624 -0
- hammad/mcp/client/client_service.py +400 -0
- hammad/mcp/client/settings.py +178 -0
- hammad/mcp/servers/__init__.py +25 -0
- hammad/mcp/servers/launcher.py +1161 -0
- hammad/runtime/__init__.py +32 -0
- hammad/runtime/decorators.py +142 -0
- hammad/runtime/run.py +299 -0
- hammad/service/__init__.py +49 -0
- hammad/service/create.py +527 -0
- hammad/service/decorators.py +285 -0
- hammad/typing/__init__.py +435 -0
- hammad/web/__init__.py +43 -0
- hammad/web/http/__init__.py +1 -0
- hammad/web/http/client.py +944 -0
- hammad/web/models.py +277 -0
- hammad/web/openapi/__init__.py +1 -0
- hammad/web/openapi/client.py +740 -0
- hammad/web/search/__init__.py +1 -0
- hammad/web/search/client.py +1035 -0
- hammad/web/utils.py +472 -0
- {hammad_python-0.0.15.dist-info → hammad_python-0.0.16.dist-info}/METADATA +8 -1
- hammad_python-0.0.16.dist-info/RECORD +110 -0
- hammad_python-0.0.15.dist-info/RECORD +0 -4
- {hammad_python-0.0.15.dist-info → hammad_python-0.0.16.dist-info}/WHEEL +0 -0
- {hammad_python-0.0.15.dist-info → hammad_python-0.0.16.dist-info}/licenses/LICENSE +0 -0
hammad/__init__.py
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
"""hammad-python"""
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
from ._internal import create_getattr_importer as __hammad_importer__
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
|
8
|
+
# hammad.cache
|
9
|
+
from .cache import (
|
10
|
+
cached,
|
11
|
+
Cache
|
12
|
+
)
|
13
|
+
|
14
|
+
# hammad.cli
|
15
|
+
from .cli import (
|
16
|
+
print,
|
17
|
+
animate,
|
18
|
+
input
|
19
|
+
)
|
20
|
+
|
21
|
+
# hammad.data
|
22
|
+
from .data.configurations import (
|
23
|
+
Configuration,
|
24
|
+
read_configuration_from_file,
|
25
|
+
read_configuration_from_url,
|
26
|
+
read_configuration_from_os_vars,
|
27
|
+
read_configuration_from_os_prefix,
|
28
|
+
read_configuration_from_dotenv
|
29
|
+
)
|
30
|
+
from .data.collections.collection import (
|
31
|
+
Collection,
|
32
|
+
create_collection
|
33
|
+
)
|
34
|
+
from .data.models import (
|
35
|
+
Model,
|
36
|
+
field,
|
37
|
+
validator,
|
38
|
+
model_settings,
|
39
|
+
convert_to_pydantic_model,
|
40
|
+
convert_to_pydantic_field,
|
41
|
+
)
|
42
|
+
from .data.types import (
|
43
|
+
Audio,
|
44
|
+
Image,
|
45
|
+
Text
|
46
|
+
)
|
47
|
+
|
48
|
+
# hammad.formatting
|
49
|
+
from .formatting.json import (
|
50
|
+
convert_to_json_schema
|
51
|
+
)
|
52
|
+
from .formatting.text import (
|
53
|
+
convert_to_text,
|
54
|
+
convert_type_to_text
|
55
|
+
)
|
56
|
+
|
57
|
+
# hammad.genai
|
58
|
+
from .genai.embedding_models import (
|
59
|
+
EmbeddingModel,
|
60
|
+
run_embedding_model,
|
61
|
+
async_run_embedding_model
|
62
|
+
)
|
63
|
+
from .genai.language_models import (
|
64
|
+
LanguageModel,
|
65
|
+
run_language_model,
|
66
|
+
async_run_language_model
|
67
|
+
)
|
68
|
+
|
69
|
+
# hammad.logging
|
70
|
+
from .logging.logger import (
|
71
|
+
Logger,
|
72
|
+
create_logger
|
73
|
+
)
|
74
|
+
from .logging.decorators import (
|
75
|
+
trace,
|
76
|
+
trace_cls,
|
77
|
+
trace_function,
|
78
|
+
trace_http
|
79
|
+
)
|
80
|
+
|
81
|
+
# hammad.service
|
82
|
+
from .service.decorators import (
|
83
|
+
serve as serve_function,
|
84
|
+
serve_mcp as serve_function_as_mcp
|
85
|
+
)
|
86
|
+
|
87
|
+
# hammad.web
|
88
|
+
from .web.http.client import (
|
89
|
+
create_http_client
|
90
|
+
)
|
91
|
+
from .web.openapi.client import (
|
92
|
+
create_openapi_client
|
93
|
+
)
|
94
|
+
from .web.search.client import (
|
95
|
+
create_search_client
|
96
|
+
)
|
97
|
+
from .web.utils import (
|
98
|
+
run_web_request,
|
99
|
+
read_web_page,
|
100
|
+
read_web_pages,
|
101
|
+
run_news_search,
|
102
|
+
run_web_search,
|
103
|
+
extract_web_page_links,
|
104
|
+
)
|
105
|
+
|
106
|
+
|
107
|
+
__all__ = (
|
108
|
+
# hammad.cache
|
109
|
+
"cached",
|
110
|
+
"Cache",
|
111
|
+
|
112
|
+
# hammad.cli
|
113
|
+
"print",
|
114
|
+
"animate",
|
115
|
+
"input",
|
116
|
+
|
117
|
+
# hammad.data
|
118
|
+
"Configuration",
|
119
|
+
"read_configuration_from_file",
|
120
|
+
"read_configuration_from_url",
|
121
|
+
"read_configuration_from_os_vars",
|
122
|
+
"read_configuration_from_os_prefix",
|
123
|
+
"read_configuration_from_dotenv",
|
124
|
+
"Collection",
|
125
|
+
"create_collection",
|
126
|
+
"Model",
|
127
|
+
"field",
|
128
|
+
"validator",
|
129
|
+
"model_settings",
|
130
|
+
"convert_to_pydantic_model",
|
131
|
+
"convert_to_pydantic_field",
|
132
|
+
"Audio",
|
133
|
+
"Image",
|
134
|
+
"Text",
|
135
|
+
|
136
|
+
# hammad.formatting
|
137
|
+
"convert_to_json_schema",
|
138
|
+
"convert_to_text",
|
139
|
+
"convert_type_to_text",
|
140
|
+
|
141
|
+
# hammad.genai
|
142
|
+
"EmbeddingModel",
|
143
|
+
"run_embedding_model",
|
144
|
+
"async_run_embedding_model",
|
145
|
+
"LanguageModel",
|
146
|
+
"run_language_model",
|
147
|
+
"async_run_language_model",
|
148
|
+
|
149
|
+
# hammad.logging
|
150
|
+
"Logger",
|
151
|
+
"create_logger",
|
152
|
+
"trace",
|
153
|
+
"trace_cls",
|
154
|
+
"trace_function",
|
155
|
+
"trace_http",
|
156
|
+
|
157
|
+
# hammad.service
|
158
|
+
"serve_function",
|
159
|
+
"serve_function_as_mcp",
|
160
|
+
|
161
|
+
# hammad.web
|
162
|
+
"create_http_client",
|
163
|
+
"create_openapi_client",
|
164
|
+
"create_search_client",
|
165
|
+
"run_web_request",
|
166
|
+
"read_web_page",
|
167
|
+
"read_web_pages",
|
168
|
+
"run_web_search",
|
169
|
+
"run_news_search",
|
170
|
+
"extract_web_page_links",
|
171
|
+
)
|
172
|
+
|
173
|
+
|
174
|
+
__getattr__ = __hammad_importer__(__all__)
|
175
|
+
|
176
|
+
|
177
|
+
def __dir__() -> list[str]:
|
178
|
+
return list(__all__)
|
hammad/_internal.py
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
"""hammad._internal
|
2
|
+
|
3
|
+
Internal utilities"""
|
4
|
+
|
5
|
+
from typing import Any, Callable, List, Tuple, Union
|
6
|
+
import inspect
|
7
|
+
import ast
|
8
|
+
import hashlib
|
9
|
+
|
10
|
+
# pretty
|
11
|
+
from rich.traceback import install
|
12
|
+
install()
|
13
|
+
|
14
|
+
__all__ = ("create_getattr_importer",)
|
15
|
+
|
16
|
+
|
17
|
+
class GetAttrImporterError(Exception):
|
18
|
+
"""An error that occurs when the `create_getattr_importer` function
|
19
|
+
fails to create a lazy loader function."""
|
20
|
+
|
21
|
+
|
22
|
+
class GetAttrImporterCache:
|
23
|
+
"""Minimal cache implementation for internal use only
|
24
|
+
within the `create_getattr_importer` function."""
|
25
|
+
|
26
|
+
def __init__(self, maxsize: int = 128):
|
27
|
+
self.maxsize = maxsize
|
28
|
+
self._cache: dict[str, Any] = {}
|
29
|
+
|
30
|
+
def _make_key(self, data: str) -> str:
|
31
|
+
"""Create a simple hash key from string data."""
|
32
|
+
return hashlib.sha256(data.encode("utf-8")).hexdigest()[:16]
|
33
|
+
|
34
|
+
def get(self, key: str, default: Any = None) -> Any:
|
35
|
+
"""Get value from cache."""
|
36
|
+
return self._cache.get(key, default)
|
37
|
+
|
38
|
+
def set(self, key: str, value: Any) -> None:
|
39
|
+
"""Set value in cache with basic LRU eviction."""
|
40
|
+
if len(self._cache) >= self.maxsize and key not in self._cache:
|
41
|
+
# Simple eviction: remove oldest (first) item
|
42
|
+
oldest_key = next(iter(self._cache))
|
43
|
+
del self._cache[oldest_key]
|
44
|
+
self._cache[key] = value
|
45
|
+
|
46
|
+
def cached_call(self, func: Callable[[str], Any]) -> Callable[[str], Any]:
|
47
|
+
"""Decorator to cache function calls."""
|
48
|
+
|
49
|
+
def wrapper(arg: str) -> Any:
|
50
|
+
key = self._make_key(arg)
|
51
|
+
result = self.get(key)
|
52
|
+
if result is None:
|
53
|
+
result = func(arg)
|
54
|
+
self.set(key, result)
|
55
|
+
return result
|
56
|
+
|
57
|
+
return wrapper
|
58
|
+
|
59
|
+
|
60
|
+
# NOTE:
|
61
|
+
# SINGLETON
|
62
|
+
GETATTR_IMPORTER_PARSE_CACHE = GetAttrImporterCache(maxsize=64)
|
63
|
+
"""Library-wide singleton instance providing caching for the
|
64
|
+
`_parse_type_checking_imports` function."""
|
65
|
+
|
66
|
+
|
67
|
+
GETATTR_IMPORTER_TYPE_CHECKING_CACHE = {}
|
68
|
+
"""Cache for the `_parse_type_checking_imports` function."""
|
69
|
+
|
70
|
+
|
71
|
+
def _parse_type_checking_imports(source_code: str) -> dict[str, tuple[str, str]]:
|
72
|
+
"""Parses the TYPE_CHECKING imports from a source code file, to create
|
73
|
+
a dictionary of local names to (module_path, original_name) tuples.
|
74
|
+
|
75
|
+
This is used to create the mapping used within the `_create_getattr_importer_from_import_dict`
|
76
|
+
function.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
source_code : The source code to parse
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
A dictionary mapping local names to (module_path, original_name) tuples
|
83
|
+
"""
|
84
|
+
|
85
|
+
@GETATTR_IMPORTER_PARSE_CACHE.cached_call
|
86
|
+
def _exec(source_code: str) -> dict[str, tuple[str, str]]:
|
87
|
+
tree = ast.parse(source_code)
|
88
|
+
imports = {}
|
89
|
+
|
90
|
+
# Walk through the AST and find TYPE_CHECKING blocks
|
91
|
+
for node in ast.walk(tree):
|
92
|
+
if isinstance(node, ast.If):
|
93
|
+
# Check if this is a TYPE_CHECKING block
|
94
|
+
is_type_checking = False
|
95
|
+
|
96
|
+
if isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING":
|
97
|
+
is_type_checking = True
|
98
|
+
elif isinstance(node.test, ast.Attribute):
|
99
|
+
if (
|
100
|
+
isinstance(node.test.value, ast.Name)
|
101
|
+
and node.test.value.id == "typing"
|
102
|
+
and node.test.attr == "TYPE_CHECKING"
|
103
|
+
):
|
104
|
+
is_type_checking = True
|
105
|
+
|
106
|
+
if is_type_checking:
|
107
|
+
# Process imports in this block
|
108
|
+
for stmt in node.body:
|
109
|
+
if isinstance(stmt, ast.ImportFrom) and stmt.module:
|
110
|
+
module_path = f".{stmt.module}"
|
111
|
+
for alias in stmt.names:
|
112
|
+
original_name = alias.name
|
113
|
+
local_name = alias.asname or original_name
|
114
|
+
imports[local_name] = (module_path, original_name)
|
115
|
+
|
116
|
+
return imports
|
117
|
+
|
118
|
+
return _exec(source_code)
|
119
|
+
|
120
|
+
|
121
|
+
def _create_getattr_importer_from_import_dict(
|
122
|
+
imports_dict: dict[str, tuple[str, str]],
|
123
|
+
package: str,
|
124
|
+
all_attrs: Union[Tuple[str, ...], List[str]],
|
125
|
+
) -> Callable[[str], Any]:
|
126
|
+
"""Creates a lazy loader function for the `__getattr__` method
|
127
|
+
within `__init__.py` modules in Python packages.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
imports_dict : Dictionary mapping attribute names to (module_path, original_name) tuples
|
131
|
+
package : The package name for import_module
|
132
|
+
all_attrs : List of all valid attributes for this module
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
A __getattr__ function that lazily imports modules
|
136
|
+
"""
|
137
|
+
from importlib import import_module
|
138
|
+
|
139
|
+
cache = {}
|
140
|
+
|
141
|
+
def __getattr__(name: str) -> Any:
|
142
|
+
if name in cache:
|
143
|
+
return cache[name]
|
144
|
+
|
145
|
+
if name in imports_dict:
|
146
|
+
module_path, original_name = imports_dict[name]
|
147
|
+
module = import_module(module_path, package)
|
148
|
+
result = getattr(module, original_name)
|
149
|
+
cache[name] = result
|
150
|
+
return result
|
151
|
+
|
152
|
+
# Try to import as a submodule
|
153
|
+
try:
|
154
|
+
module_path = f".{name}"
|
155
|
+
module = import_module(module_path, package)
|
156
|
+
cache[name] = module
|
157
|
+
return module
|
158
|
+
except ImportError:
|
159
|
+
pass
|
160
|
+
|
161
|
+
raise GetAttrImporterError(f"module '{package}' has no attribute '{name}'")
|
162
|
+
|
163
|
+
return __getattr__
|
164
|
+
|
165
|
+
|
166
|
+
def create_getattr_importer(
|
167
|
+
all: Union[Tuple[str, ...], List[str]],
|
168
|
+
) -> Callable[[str], Any]:
|
169
|
+
"""Loader used internally within the `hammad` package to create lazy
|
170
|
+
loaders within `__init__.py` modules using the `TYPE_CHECKING` and
|
171
|
+
`all` source code within files.
|
172
|
+
|
173
|
+
This function is meant to be set as the `__getattr__` method / var
|
174
|
+
within modules to allow for direct lazy loading of attributes.
|
175
|
+
|
176
|
+
Example:
|
177
|
+
|
178
|
+
```
|
179
|
+
# Create a module that contains some imports and TYPE_CHECKING
|
180
|
+
from typing import TYPE_CHECKING
|
181
|
+
from hammad.performance.imports import create_getattr_importer
|
182
|
+
|
183
|
+
if TYPE_CHECKING:
|
184
|
+
from functools import wraps
|
185
|
+
|
186
|
+
all = ("wraps")
|
187
|
+
|
188
|
+
__getattr__ = create_getattr_importer(all)
|
189
|
+
|
190
|
+
# Now, when you import this module, the `wraps` attribute will be
|
191
|
+
# lazily loaded when it is first accessed.
|
192
|
+
```
|
193
|
+
|
194
|
+
Args:
|
195
|
+
all : The `all` tuple from the calling module
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
A __getattr__ function that lazily imports modules
|
199
|
+
"""
|
200
|
+
# Get the calling module's frame
|
201
|
+
frame = inspect.currentframe()
|
202
|
+
if frame is None or frame.f_back is None:
|
203
|
+
raise RuntimeError("Cannot determine calling module")
|
204
|
+
|
205
|
+
calling_frame = frame.f_back
|
206
|
+
module_name = calling_frame.f_globals.get("__name__", "")
|
207
|
+
package = calling_frame.f_globals.get("__package__", "")
|
208
|
+
filename = calling_frame.f_globals.get("__file__", "")
|
209
|
+
|
210
|
+
# Check cache first
|
211
|
+
cache_key = (filename, tuple(all))
|
212
|
+
if cache_key in GETATTR_IMPORTER_TYPE_CHECKING_CACHE:
|
213
|
+
return GETATTR_IMPORTER_TYPE_CHECKING_CACHE[cache_key]
|
214
|
+
|
215
|
+
# Read the source file
|
216
|
+
try:
|
217
|
+
with open(filename, "r") as f:
|
218
|
+
source_code = f.read()
|
219
|
+
except (IOError, OSError):
|
220
|
+
# Fallback: try to get source from the module
|
221
|
+
import sys
|
222
|
+
|
223
|
+
module = sys.modules.get(module_name)
|
224
|
+
if module:
|
225
|
+
source_code = inspect.getsource(module)
|
226
|
+
else:
|
227
|
+
raise RuntimeError(f"Cannot read source for module {module_name}")
|
228
|
+
|
229
|
+
# Parse the source to extract TYPE_CHECKING imports
|
230
|
+
imports_map = _parse_type_checking_imports(source_code)
|
231
|
+
|
232
|
+
# Filter to only include exports that are in __all__
|
233
|
+
filtered_map = {name: path for name, path in imports_map.items() if name in all}
|
234
|
+
|
235
|
+
loader = _create_getattr_importer_from_import_dict(filtered_map, package, all)
|
236
|
+
GETATTR_IMPORTER_TYPE_CHECKING_CACHE[cache_key] = loader
|
237
|
+
return loader
|
hammad/cache/__init__.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
"""hammad.cache"""
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
from .._internal import create_getattr_importer
|
5
|
+
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from .base_cache import BaseCache, CacheParams, CacheReturn, CacheType
|
9
|
+
from .file_cache import FileCache, FileCacheLocation
|
10
|
+
from .ttl_cache import TTLCache
|
11
|
+
from .cache import Cache, create_cache
|
12
|
+
from .decorators import cached, auto_cached, clear_decorator_cache
|
13
|
+
|
14
|
+
|
15
|
+
__all__ = (
|
16
|
+
# hammad.performance.cache.base_cache
|
17
|
+
"BaseCache",
|
18
|
+
"CacheParams",
|
19
|
+
"CacheReturn",
|
20
|
+
"CacheType",
|
21
|
+
# hammad.performance.cache.file_cache
|
22
|
+
"FileCache",
|
23
|
+
"FileCacheLocation",
|
24
|
+
# hammad.performance.cache.ttl_cache
|
25
|
+
"TTLCache",
|
26
|
+
# hammad.performance.cache.cache
|
27
|
+
"Cache",
|
28
|
+
"create_cache",
|
29
|
+
# hammad.performance.cache.decorators
|
30
|
+
"cached",
|
31
|
+
"auto_cached",
|
32
|
+
"clear_decorator_cache",
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
__getattr__ = create_getattr_importer(__all__)
|
37
|
+
|
38
|
+
|
39
|
+
def __dir__() -> list[str]:
|
40
|
+
return sorted(__all__)
|
@@ -0,0 +1,181 @@
|
|
1
|
+
"""hammad.cache.base_cache"""
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
import hashlib
|
5
|
+
import inspect
|
6
|
+
from typing import Any, Literal, ParamSpec, TypeAlias, TypeVar, get_args
|
7
|
+
|
8
|
+
__all__ = (
|
9
|
+
"BaseCache",
|
10
|
+
"CacheType",
|
11
|
+
"CacheParams",
|
12
|
+
"CacheReturn",
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
CacheType: TypeAlias = Literal["ttl", "file"]
|
17
|
+
"""Type of caches that can be created using `hammad`.
|
18
|
+
|
19
|
+
- `"ttl"`: Time-to-live cache.
|
20
|
+
- `"file"`: File-based cache.
|
21
|
+
"""
|
22
|
+
|
23
|
+
CacheParams = ParamSpec("CacheParams")
|
24
|
+
"""Parameter specification for cache functions."""
|
25
|
+
|
26
|
+
CacheReturn = TypeVar("CacheReturn")
|
27
|
+
"""Return type for cache functions."""
|
28
|
+
|
29
|
+
|
30
|
+
@dataclass
|
31
|
+
class BaseCache:
|
32
|
+
"""Base class for all caches created using `hammad`."""
|
33
|
+
|
34
|
+
type: CacheType
|
35
|
+
"""Type of cache."""
|
36
|
+
|
37
|
+
def __post_init__(self) -> None:
|
38
|
+
"""Post-initialization hook."""
|
39
|
+
if self.type not in get_args(CacheType):
|
40
|
+
raise ValueError(f"Invalid cache type: {self.type}")
|
41
|
+
|
42
|
+
def __contains__(self, key: str) -> bool:
|
43
|
+
"""Check if key exists in cache."""
|
44
|
+
raise NotImplementedError("Subclasses must implement __contains__")
|
45
|
+
|
46
|
+
def __getitem__(self, key: str) -> Any:
|
47
|
+
"""Get value for key."""
|
48
|
+
raise NotImplementedError("Subclasses must implement __getitem__")
|
49
|
+
|
50
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
51
|
+
"""Set value for key."""
|
52
|
+
raise NotImplementedError("Subclasses must implement __setitem__")
|
53
|
+
|
54
|
+
def get(self, key: str, default: Any = None) -> Any:
|
55
|
+
"""Get value with default if key doesn't exist."""
|
56
|
+
try:
|
57
|
+
return self[key]
|
58
|
+
except KeyError:
|
59
|
+
return default
|
60
|
+
|
61
|
+
def clear(self) -> None:
|
62
|
+
"""Clear all cached items."""
|
63
|
+
raise NotImplementedError("Subclasses must implement clear")
|
64
|
+
|
65
|
+
def make_hashable(self, obj: Any) -> str:
|
66
|
+
"""
|
67
|
+
Convert any object to a stable hash string.
|
68
|
+
|
69
|
+
Uses SHA-256 to generate consistent hash representations.
|
70
|
+
Handles nested structures recursively.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
obj: Object to hash
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
Hexadecimal hash string
|
77
|
+
"""
|
78
|
+
|
79
|
+
def _hash_obj(data: Any) -> str:
|
80
|
+
"""Internal recursive hashing function with memoization."""
|
81
|
+
# Handle None first
|
82
|
+
if data is None:
|
83
|
+
return "null"
|
84
|
+
|
85
|
+
if isinstance(data, bool):
|
86
|
+
return f"bool:{data}"
|
87
|
+
elif isinstance(data, int):
|
88
|
+
return f"int:{data}"
|
89
|
+
elif isinstance(data, float):
|
90
|
+
if data != data: # NaN
|
91
|
+
return "float:nan"
|
92
|
+
elif data == float("inf"):
|
93
|
+
return "float:inf"
|
94
|
+
elif data == float("-inf"):
|
95
|
+
return "float:-inf"
|
96
|
+
else:
|
97
|
+
return f"float:{data}"
|
98
|
+
elif isinstance(data, str):
|
99
|
+
return f"str:{data}"
|
100
|
+
elif isinstance(data, bytes):
|
101
|
+
return f"bytes:{data.hex()}"
|
102
|
+
|
103
|
+
# Handle collections
|
104
|
+
elif isinstance(data, (list, tuple)):
|
105
|
+
collection_type = "list" if isinstance(data, list) else "tuple"
|
106
|
+
items = [_hash_obj(item) for item in data]
|
107
|
+
return f"{collection_type}:[{','.join(items)}]"
|
108
|
+
|
109
|
+
elif isinstance(data, set):
|
110
|
+
try:
|
111
|
+
sorted_items = sorted(data, key=lambda x: str(x))
|
112
|
+
except TypeError:
|
113
|
+
sorted_items = sorted(
|
114
|
+
data, key=lambda x: (type(x).__name__, str(x))
|
115
|
+
)
|
116
|
+
items = [_hash_obj(item) for item in sorted_items]
|
117
|
+
return f"set:{{{','.join(items)}}}"
|
118
|
+
|
119
|
+
elif isinstance(data, dict):
|
120
|
+
try:
|
121
|
+
sorted_items = sorted(data.items(), key=lambda x: str(x[0]))
|
122
|
+
except TypeError:
|
123
|
+
# Fallback for non-comparable keys
|
124
|
+
sorted_items = sorted(
|
125
|
+
data.items(), key=lambda x: (type(x[0]).__name__, str(x[0]))
|
126
|
+
)
|
127
|
+
pairs = [f"{_hash_obj(k)}:{_hash_obj(v)}" for k, v in sorted_items]
|
128
|
+
return f"dict:{{{','.join(pairs)}}}"
|
129
|
+
|
130
|
+
elif isinstance(data, type):
|
131
|
+
module = getattr(data, "__module__", "builtins")
|
132
|
+
qualname = getattr(data, "__qualname__", data.__name__)
|
133
|
+
return f"type:{module}.{qualname}"
|
134
|
+
|
135
|
+
elif callable(data):
|
136
|
+
module = getattr(data, "__module__", "unknown")
|
137
|
+
qualname = getattr(
|
138
|
+
data, "__qualname__", getattr(data, "__name__", "unknown_callable")
|
139
|
+
)
|
140
|
+
|
141
|
+
try:
|
142
|
+
source = inspect.getsource(data)
|
143
|
+
normalized_source = " ".join(source.split())
|
144
|
+
return f"callable:{module}.{qualname}:{hash(normalized_source)}"
|
145
|
+
except (OSError, TypeError, IndentationError):
|
146
|
+
return f"callable:{module}.{qualname}"
|
147
|
+
|
148
|
+
elif hasattr(data, "__dict__"):
|
149
|
+
class_info = (
|
150
|
+
f"{data.__class__.__module__}.{data.__class__.__qualname__}"
|
151
|
+
)
|
152
|
+
obj_dict = {"__class__": class_info, **data.__dict__}
|
153
|
+
return f"object:{_hash_obj(obj_dict)}"
|
154
|
+
|
155
|
+
elif hasattr(data, "__slots__"):
|
156
|
+
class_info = (
|
157
|
+
f"{data.__class__.__module__}.{data.__class__.__qualname__}"
|
158
|
+
)
|
159
|
+
slot_dict = {
|
160
|
+
slot: getattr(data, slot, None)
|
161
|
+
for slot in data.__slots__
|
162
|
+
if hasattr(data, slot)
|
163
|
+
}
|
164
|
+
obj_dict = {"__class__": class_info, **slot_dict}
|
165
|
+
return f"slotted_object:{_hash_obj(obj_dict)}"
|
166
|
+
|
167
|
+
else:
|
168
|
+
try:
|
169
|
+
repr_str = repr(data)
|
170
|
+
return f"repr:{type(data).__name__}:{repr_str}"
|
171
|
+
except Exception:
|
172
|
+
# Ultimate fallback
|
173
|
+
return f"unknown:{type(data).__name__}:{id(data)}"
|
174
|
+
|
175
|
+
# Generate the hash representation
|
176
|
+
hash_representation = _hash_obj(obj)
|
177
|
+
|
178
|
+
# Create final SHA-256 hash
|
179
|
+
return hashlib.sha256(
|
180
|
+
hash_representation.encode("utf-8", errors="surrogatepass")
|
181
|
+
).hexdigest()
|