naas-abi-core 1.4.1__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.
- assets/favicon.ico +0 -0
- assets/logo.png +0 -0
- naas_abi_core/__init__.py +1 -0
- naas_abi_core/apps/api/api.py +245 -0
- naas_abi_core/apps/api/api_test.py +281 -0
- naas_abi_core/apps/api/openapi_doc.py +144 -0
- naas_abi_core/apps/mcp/Dockerfile.mcp +35 -0
- naas_abi_core/apps/mcp/mcp_server.py +243 -0
- naas_abi_core/apps/mcp/mcp_server_test.py +163 -0
- naas_abi_core/apps/terminal_agent/main.py +555 -0
- naas_abi_core/apps/terminal_agent/terminal_style.py +175 -0
- naas_abi_core/engine/Engine.py +87 -0
- naas_abi_core/engine/EngineProxy.py +109 -0
- naas_abi_core/engine/Engine_test.py +6 -0
- naas_abi_core/engine/IEngine.py +91 -0
- naas_abi_core/engine/conftest.py +45 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration.py +216 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_Deploy.py +7 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_GenericLoader.py +49 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService.py +159 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_ObjectStorageService_test.py +26 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService.py +138 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_SecretService_test.py +74 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService.py +224 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_TripleStoreService_test.py +109 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService.py +76 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_VectorStoreService_test.py +33 -0
- naas_abi_core/engine/engine_configuration/EngineConfiguration_test.py +9 -0
- naas_abi_core/engine/engine_configuration/utils/PydanticModelValidator.py +15 -0
- naas_abi_core/engine/engine_loaders/EngineModuleLoader.py +302 -0
- naas_abi_core/engine/engine_loaders/EngineOntologyLoader.py +16 -0
- naas_abi_core/engine/engine_loaders/EngineServiceLoader.py +47 -0
- naas_abi_core/integration/__init__.py +7 -0
- naas_abi_core/integration/integration.py +28 -0
- naas_abi_core/models/Model.py +198 -0
- naas_abi_core/models/OpenRouter.py +18 -0
- naas_abi_core/models/OpenRouter_test.py +36 -0
- naas_abi_core/module/Module.py +252 -0
- naas_abi_core/module/ModuleAgentLoader.py +50 -0
- naas_abi_core/module/ModuleUtils.py +20 -0
- naas_abi_core/modules/templatablesparqlquery/README.md +196 -0
- naas_abi_core/modules/templatablesparqlquery/__init__.py +39 -0
- naas_abi_core/modules/templatablesparqlquery/ontologies/TemplatableSparqlQueryOntology.ttl +116 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/GenericWorkflow.py +48 -0
- naas_abi_core/modules/templatablesparqlquery/workflows/TemplatableSparqlQueryLoader.py +192 -0
- naas_abi_core/pipeline/__init__.py +6 -0
- naas_abi_core/pipeline/pipeline.py +70 -0
- naas_abi_core/services/__init__.py +0 -0
- naas_abi_core/services/agent/Agent.py +1619 -0
- naas_abi_core/services/agent/AgentMemory_test.py +28 -0
- naas_abi_core/services/agent/Agent_test.py +214 -0
- naas_abi_core/services/agent/IntentAgent.py +1179 -0
- naas_abi_core/services/agent/IntentAgent_test.py +139 -0
- naas_abi_core/services/agent/beta/Embeddings.py +181 -0
- naas_abi_core/services/agent/beta/IntentMapper.py +120 -0
- naas_abi_core/services/agent/beta/LocalModel.py +88 -0
- naas_abi_core/services/agent/beta/VectorStore.py +89 -0
- naas_abi_core/services/agent/test_agent_memory.py +278 -0
- naas_abi_core/services/agent/test_postgres_integration.py +145 -0
- naas_abi_core/services/cache/CacheFactory.py +31 -0
- naas_abi_core/services/cache/CachePort.py +63 -0
- naas_abi_core/services/cache/CacheService.py +246 -0
- naas_abi_core/services/cache/CacheService_test.py +85 -0
- naas_abi_core/services/cache/adapters/secondary/CacheFSAdapter.py +39 -0
- naas_abi_core/services/object_storage/ObjectStorageFactory.py +57 -0
- naas_abi_core/services/object_storage/ObjectStoragePort.py +47 -0
- naas_abi_core/services/object_storage/ObjectStorageService.py +41 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterFS.py +52 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterNaas.py +131 -0
- naas_abi_core/services/object_storage/adapters/secondary/ObjectStorageSecondaryAdapterS3.py +171 -0
- naas_abi_core/services/ontology/OntologyPorts.py +36 -0
- naas_abi_core/services/ontology/OntologyService.py +17 -0
- naas_abi_core/services/ontology/adaptors/secondary/OntologyService_SecondaryAdaptor_NERPort.py +37 -0
- naas_abi_core/services/secret/Secret.py +138 -0
- naas_abi_core/services/secret/SecretPorts.py +45 -0
- naas_abi_core/services/secret/Secret_test.py +65 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret.py +57 -0
- naas_abi_core/services/secret/adaptors/secondary/Base64Secret_test.py +39 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret.py +88 -0
- naas_abi_core/services/secret/adaptors/secondary/NaasSecret_test.py +25 -0
- naas_abi_core/services/secret/adaptors/secondary/dotenv_secret_secondaryadaptor.py +29 -0
- naas_abi_core/services/triple_store/TripleStoreFactory.py +116 -0
- naas_abi_core/services/triple_store/TripleStorePorts.py +223 -0
- naas_abi_core/services/triple_store/TripleStoreService.py +419 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune.py +1300 -0
- naas_abi_core/services/triple_store/adaptors/secondary/AWSNeptune_test.py +284 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph.py +597 -0
- naas_abi_core/services/triple_store/adaptors/secondary/Oxigraph_test.py +1474 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__Filesystem.py +223 -0
- naas_abi_core/services/triple_store/adaptors/secondary/TripleStoreService__SecondaryAdaptor__ObjectStorage.py +234 -0
- naas_abi_core/services/triple_store/adaptors/secondary/base/TripleStoreService__SecondaryAdaptor__FileBase.py +18 -0
- naas_abi_core/services/vector_store/IVectorStorePort.py +101 -0
- naas_abi_core/services/vector_store/IVectorStorePort_test.py +189 -0
- naas_abi_core/services/vector_store/VectorStoreFactory.py +47 -0
- naas_abi_core/services/vector_store/VectorStoreService.py +171 -0
- naas_abi_core/services/vector_store/VectorStoreService_test.py +185 -0
- naas_abi_core/services/vector_store/__init__.py +13 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter.py +251 -0
- naas_abi_core/services/vector_store/adapters/QdrantAdapter_test.py +57 -0
- naas_abi_core/tests/test_services_imports.py +69 -0
- naas_abi_core/utils/Expose.py +55 -0
- naas_abi_core/utils/Graph.py +182 -0
- naas_abi_core/utils/JSON.py +49 -0
- naas_abi_core/utils/LazyLoader.py +44 -0
- naas_abi_core/utils/Logger.py +12 -0
- naas_abi_core/utils/OntologyReasoner.py +141 -0
- naas_abi_core/utils/OntologyYaml.py +681 -0
- naas_abi_core/utils/SPARQL.py +256 -0
- naas_abi_core/utils/Storage.py +33 -0
- naas_abi_core/utils/StorageUtils.py +398 -0
- naas_abi_core/utils/String.py +52 -0
- naas_abi_core/utils/Workers.py +114 -0
- naas_abi_core/utils/__init__.py +0 -0
- naas_abi_core/utils/onto2py/README.md +0 -0
- naas_abi_core/utils/onto2py/__init__.py +10 -0
- naas_abi_core/utils/onto2py/__main__.py +29 -0
- naas_abi_core/utils/onto2py/onto2py.py +611 -0
- naas_abi_core/utils/onto2py/tests/ttl2py_test.py +271 -0
- naas_abi_core/workflow/__init__.py +5 -0
- naas_abi_core/workflow/workflow.py +48 -0
- naas_abi_core-1.4.1.dist-info/METADATA +630 -0
- naas_abi_core-1.4.1.dist-info/RECORD +124 -0
- naas_abi_core-1.4.1.dist-info/WHEEL +4 -0
- naas_abi_core-1.4.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import datetime
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import pickle
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from naas_abi_core import logger
|
|
10
|
+
from naas_abi_core.services.cache.CachePort import (
|
|
11
|
+
CachedData,
|
|
12
|
+
CacheExpiredError,
|
|
13
|
+
CacheNotFoundError,
|
|
14
|
+
DataType,
|
|
15
|
+
ICacheAdapter,
|
|
16
|
+
ICacheService,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ForceRefresh(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CacheService(ICacheService):
|
|
25
|
+
def __init__(self, adapter: ICacheAdapter):
|
|
26
|
+
self.adapter = adapter
|
|
27
|
+
|
|
28
|
+
self.deserializers = {
|
|
29
|
+
DataType.TEXT: self.__get_text,
|
|
30
|
+
DataType.JSON: self.__get_json,
|
|
31
|
+
DataType.BINARY: self.__get_binary,
|
|
32
|
+
DataType.PICKLE: self.__get_pickle,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def __call__(
|
|
36
|
+
self,
|
|
37
|
+
key_builder: Callable,
|
|
38
|
+
cache_type: DataType,
|
|
39
|
+
ttl: datetime.timedelta | None = None,
|
|
40
|
+
auto_cache: bool = True,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Decorator to cache function results.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
key_builder: Function that takes the same parameters as the decorated function
|
|
47
|
+
and returns a cache key (string or dict)
|
|
48
|
+
cache_type: Cache type to use (TEXT, JSON, BINARY, PICKLE).
|
|
49
|
+
Required when auto_cache=True (default: None)
|
|
50
|
+
ttl: Time-to-live for cached data. If specified, cached data will expire
|
|
51
|
+
after this duration (default: None)
|
|
52
|
+
auto_cache: Whether to automatically cache the result (default: True)
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> # Create cache service instance
|
|
56
|
+
>>> cache_service = CacheService(adapter)
|
|
57
|
+
>>>
|
|
58
|
+
>>> # Apply cache decorator with explicit cache type
|
|
59
|
+
>>> @cache_service(lambda user_id, include_profile=False: f"user_{user_id}_profile_{include_profile}",
|
|
60
|
+
... cache_type=DataType.JSON)
|
|
61
|
+
... def get_user_data(user_id: int, include_profile: bool = False):
|
|
62
|
+
... # Expensive operation (e.g., database query, API call)
|
|
63
|
+
... return {"id": user_id, "name": "John Doe", "profile": include_profile}
|
|
64
|
+
>>>
|
|
65
|
+
>>> # Disable auto-caching (only retrieves from cache, doesn't store)
|
|
66
|
+
>>> @cache_service(lambda x: f"key_{x}", cache_type=DataType.TEXT, auto_cache=False)
|
|
67
|
+
... def get_data_no_auto_cache(x):
|
|
68
|
+
... return f"data_{x}"
|
|
69
|
+
>>>
|
|
70
|
+
>>> # Cache text data explicitly
|
|
71
|
+
>>> @cache_service(lambda x: f"key_{x}", cache_type=DataType.TEXT)
|
|
72
|
+
... def get_text_data(x):
|
|
73
|
+
... return f"data_{x}"
|
|
74
|
+
>>>
|
|
75
|
+
>>> # Cache with TTL (expires after 1 hour)
|
|
76
|
+
>>> @cache_service(lambda x: f"key_{x}", cache_type=DataType.JSON,
|
|
77
|
+
... ttl=datetime.timedelta(hours=1))
|
|
78
|
+
... def get_data_with_ttl(x):
|
|
79
|
+
... return {"data": x, "timestamp": datetime.datetime.now()}
|
|
80
|
+
>>>
|
|
81
|
+
>>> # Force cache refresh with special parameter
|
|
82
|
+
>>> result = get_user_data(123, True, force_cache_refresh=True) # Bypasses cache
|
|
83
|
+
>>>
|
|
84
|
+
>>> # First call - executes function and caches result
|
|
85
|
+
>>> result1 = get_user_data(123, True) # Cache key: "user_123_profile_True"
|
|
86
|
+
>>>
|
|
87
|
+
>>> # Second call - returns cached result
|
|
88
|
+
>>> result2 = get_user_data(123, True) # Returns from cache
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
logger.debug(
|
|
92
|
+
f"Cache decorator called with key_builder: {key_builder}, auto_cache: {auto_cache}, cache_type: {cache_type}, ttl: {ttl}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def decorator(func):
|
|
96
|
+
def wrapper(*args, **kwargs):
|
|
97
|
+
# Step 1: Create a complete mapping of all function arguments
|
|
98
|
+
# This will contain all arguments passed to the decorated function,
|
|
99
|
+
# including positional args, keyword args, and default values
|
|
100
|
+
mapped_args = OrderedDict()
|
|
101
|
+
func_args = inspect.signature(func).parameters
|
|
102
|
+
func_args_list = list(func_args.keys())
|
|
103
|
+
|
|
104
|
+
# Step 2: Map positional arguments to their parameter names
|
|
105
|
+
# Convert positional args like func(a, b, c) to named args like {'x': a, 'y': b, 'z': c}
|
|
106
|
+
for arg_index in range(len(args)):
|
|
107
|
+
arg_name = func_args_list[arg_index]
|
|
108
|
+
mapped_args[arg_name] = args[arg_index]
|
|
109
|
+
|
|
110
|
+
# Step 3: Add keyword arguments to the mapping
|
|
111
|
+
# Only include kwargs that are actually parameters of the decorated function
|
|
112
|
+
for arg_name, arg_value in kwargs.items():
|
|
113
|
+
if arg_name in func_args_list:
|
|
114
|
+
mapped_args[arg_name] = arg_value
|
|
115
|
+
|
|
116
|
+
# Step 4: Fill in default values for parameters that weren't provided
|
|
117
|
+
# This ensures we have a complete picture of all function arguments
|
|
118
|
+
for arg_name, arg_value in func_args.items():
|
|
119
|
+
if (
|
|
120
|
+
arg_value.default is not arg_value.empty
|
|
121
|
+
and arg_name not in mapped_args
|
|
122
|
+
):
|
|
123
|
+
mapped_args[arg_name] = arg_value.default
|
|
124
|
+
|
|
125
|
+
# Step 5: Filter arguments to only include those needed by the key_builder
|
|
126
|
+
# The key_builder function may only need a subset of the decorated function's arguments
|
|
127
|
+
key_builder_args = inspect.signature(key_builder).parameters
|
|
128
|
+
filtered_args = OrderedDict()
|
|
129
|
+
|
|
130
|
+
# Only pass arguments that the key_builder function expects
|
|
131
|
+
for arg_name, arg_value in mapped_args.items():
|
|
132
|
+
if arg_name in key_builder_args:
|
|
133
|
+
filtered_args[arg_name] = arg_value
|
|
134
|
+
|
|
135
|
+
# Build cache key using the provided key_builder function
|
|
136
|
+
cache_key = key_builder(**filtered_args)
|
|
137
|
+
|
|
138
|
+
# Try to get from cache first
|
|
139
|
+
try:
|
|
140
|
+
if "force_cache_refresh" in kwargs:
|
|
141
|
+
del kwargs["force_cache_refresh"]
|
|
142
|
+
raise ForceRefresh()
|
|
143
|
+
|
|
144
|
+
cached_data = self.__get_cached_data(cache_key, ttl)
|
|
145
|
+
|
|
146
|
+
if cached_data.data_type == cache_type:
|
|
147
|
+
return self.deserializers[cache_type](cached_data)
|
|
148
|
+
else:
|
|
149
|
+
raise CacheNotFoundError(
|
|
150
|
+
f"Cache Data Type change from {cached_data.data_type} to {cache_type}."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
except (CacheNotFoundError, CacheExpiredError, ForceRefresh):
|
|
154
|
+
# If not in cache or expired, execute function
|
|
155
|
+
result = func(*args, **kwargs)
|
|
156
|
+
|
|
157
|
+
# Cache result if auto_cache is enabled
|
|
158
|
+
if auto_cache:
|
|
159
|
+
# Use specified cache_type
|
|
160
|
+
if cache_type == DataType.TEXT:
|
|
161
|
+
self.set_text(cache_key, result)
|
|
162
|
+
elif cache_type == DataType.JSON:
|
|
163
|
+
self.set_json(cache_key, result)
|
|
164
|
+
elif cache_type == DataType.BINARY:
|
|
165
|
+
self.set_binary(cache_key, result)
|
|
166
|
+
elif cache_type == DataType.PICKLE:
|
|
167
|
+
self.set_pickle(cache_key, result)
|
|
168
|
+
else:
|
|
169
|
+
# Require explicit cache type specification
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"cache_type must be explicitly specified. "
|
|
172
|
+
f"Result type: {type(result).__name__}. "
|
|
173
|
+
f"Available types: {[dt.name for dt in DataType]}"
|
|
174
|
+
f"Cache key: {cache_key}"
|
|
175
|
+
f"cache_type: {cache_type}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
return wrapper
|
|
181
|
+
|
|
182
|
+
return decorator
|
|
183
|
+
|
|
184
|
+
def __get_cached_data(
|
|
185
|
+
self, key: str, ttl: datetime.timedelta | None = None
|
|
186
|
+
) -> CachedData:
|
|
187
|
+
try:
|
|
188
|
+
cached_data = self.adapter.get(key)
|
|
189
|
+
except (CacheNotFoundError, Exception) as _:
|
|
190
|
+
raise CacheNotFoundError(f"Cache not found: {key}")
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
ttl
|
|
194
|
+
and datetime.datetime.fromisoformat(cached_data.created_at) + ttl
|
|
195
|
+
< datetime.datetime.now()
|
|
196
|
+
):
|
|
197
|
+
raise CacheExpiredError(
|
|
198
|
+
f"Cache expired: {key}. TTL: {ttl}. Created at: {cached_data.created_at}."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return cached_data
|
|
202
|
+
|
|
203
|
+
def get(self, key: str, ttl: datetime.timedelta | None = None) -> Any:
|
|
204
|
+
cached_data = self.__get_cached_data(key, ttl)
|
|
205
|
+
return self.deserializers[cached_data.data_type](cached_data)
|
|
206
|
+
|
|
207
|
+
def __get_text(self, data: CachedData) -> str:
|
|
208
|
+
return data.data
|
|
209
|
+
|
|
210
|
+
def __get_json(self, data: CachedData) -> dict:
|
|
211
|
+
return json.loads(data.data)
|
|
212
|
+
|
|
213
|
+
def __get_binary(self, data: CachedData) -> bytes:
|
|
214
|
+
return base64.b64decode(data.data)
|
|
215
|
+
|
|
216
|
+
def __get_pickle(self, data: CachedData) -> Any:
|
|
217
|
+
return pickle.loads(base64.b64decode(data.data))
|
|
218
|
+
|
|
219
|
+
def set_text(self, key: str, value: str) -> None:
|
|
220
|
+
assert isinstance(value, str), f"Value must be a string. Got {type(value)}"
|
|
221
|
+
cached_data = CachedData(key=key, data=value, data_type=DataType.TEXT)
|
|
222
|
+
self.adapter.set(key, cached_data)
|
|
223
|
+
|
|
224
|
+
def set_json(self, key: str, value: Any) -> None:
|
|
225
|
+
cached_data = CachedData(
|
|
226
|
+
key=key, data=json.dumps(value), data_type=DataType.JSON
|
|
227
|
+
)
|
|
228
|
+
self.adapter.set(key, cached_data)
|
|
229
|
+
|
|
230
|
+
def set_binary(self, key: str, value: bytes) -> None:
|
|
231
|
+
assert isinstance(value, bytes), f"Value must be a bytes. Got {type(value)}"
|
|
232
|
+
cached_data = CachedData(
|
|
233
|
+
key=key, data=base64.b64encode(value).decode(), data_type=DataType.BINARY
|
|
234
|
+
)
|
|
235
|
+
self.adapter.set(key, cached_data)
|
|
236
|
+
|
|
237
|
+
def set_pickle(self, key: str, value: Any) -> None:
|
|
238
|
+
cached_data = CachedData(
|
|
239
|
+
key=key,
|
|
240
|
+
data=base64.b64encode(pickle.dumps(value)).decode(),
|
|
241
|
+
data_type=DataType.PICKLE,
|
|
242
|
+
)
|
|
243
|
+
self.adapter.set(key, cached_data)
|
|
244
|
+
|
|
245
|
+
def exists(self, key: str) -> bool:
|
|
246
|
+
return self.adapter.exists(key)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from naas_abi_core.services.cache.CachePort import (
|
|
5
|
+
CachedData,
|
|
6
|
+
CacheNotFoundError,
|
|
7
|
+
DataType,
|
|
8
|
+
ICacheAdapter,
|
|
9
|
+
)
|
|
10
|
+
from naas_abi_core.services.cache.CacheService import CacheService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CacheMemoryAdapter(ICacheAdapter):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.cache = {}
|
|
16
|
+
|
|
17
|
+
def get(self, key: str) -> CachedData:
|
|
18
|
+
if key not in self.cache:
|
|
19
|
+
raise CacheNotFoundError(f"Cache not found: {key}")
|
|
20
|
+
return self.cache[key]
|
|
21
|
+
|
|
22
|
+
def set(self, key: str, value: CachedData) -> None:
|
|
23
|
+
self.cache[key] = value
|
|
24
|
+
|
|
25
|
+
def delete(self, key: str) -> None:
|
|
26
|
+
if key not in self.cache:
|
|
27
|
+
raise CacheNotFoundError(f"Cache not found: {key}")
|
|
28
|
+
del self.cache[key]
|
|
29
|
+
|
|
30
|
+
def exists(self, key: str) -> bool:
|
|
31
|
+
return key in self.cache
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_cache_service():
|
|
35
|
+
cache_service = CacheService(CacheMemoryAdapter())
|
|
36
|
+
|
|
37
|
+
salt = "good"
|
|
38
|
+
|
|
39
|
+
@cache_service(
|
|
40
|
+
lambda x: f"key_{x}",
|
|
41
|
+
cache_type=DataType.TEXT,
|
|
42
|
+
ttl=datetime.timedelta(seconds=1),
|
|
43
|
+
)
|
|
44
|
+
def get_text_data(x: str) -> str:
|
|
45
|
+
"""Get text data"""
|
|
46
|
+
return f"data_{x}_{salt}"
|
|
47
|
+
|
|
48
|
+
# First call will not be cached and will populate it
|
|
49
|
+
assert get_text_data("test") == "data_test_good"
|
|
50
|
+
|
|
51
|
+
# We change the salt, so if the method is executed again, the assertion will fail. Here we want to
|
|
52
|
+
# make sure that we are getting the data from the cache.
|
|
53
|
+
salt = "bad"
|
|
54
|
+
|
|
55
|
+
assert get_text_data("test") == "data_test_good"
|
|
56
|
+
|
|
57
|
+
# We sleep for 1 second to make sure the cache will expire.
|
|
58
|
+
time.sleep(1)
|
|
59
|
+
|
|
60
|
+
assert get_text_data("test") == "data_test_bad"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_non_matching_arguments():
|
|
64
|
+
cache_service = CacheService(CacheMemoryAdapter())
|
|
65
|
+
|
|
66
|
+
@cache_service(
|
|
67
|
+
lambda x, z: f"key_{x}_{z}",
|
|
68
|
+
cache_type=DataType.TEXT,
|
|
69
|
+
ttl=datetime.timedelta(seconds=1),
|
|
70
|
+
)
|
|
71
|
+
def get_text_data(x: str, y: str, z: str = "toto") -> str:
|
|
72
|
+
"""Get text data"""
|
|
73
|
+
return f"{x}_{y}_{z}"
|
|
74
|
+
|
|
75
|
+
assert get_text_data("test", "test", "tati") == "test_test_tati"
|
|
76
|
+
assert get_text_data(x="test", y="tata", z="tutu") == "test_tata_tutu"
|
|
77
|
+
assert get_text_data(x="test", y="toto") == "test_toto_toto"
|
|
78
|
+
|
|
79
|
+
# There is cache
|
|
80
|
+
assert get_text_data(x="test", y="yolo") == "test_toto_toto"
|
|
81
|
+
|
|
82
|
+
# Same but for cache refresh
|
|
83
|
+
assert (
|
|
84
|
+
get_text_data(x="test", y="yolo", force_cache_refresh=True) == "test_yolo_toto"
|
|
85
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from naas_abi_core.services.cache.CachePort import (
|
|
6
|
+
CachedData,
|
|
7
|
+
CacheNotFoundError,
|
|
8
|
+
ICacheAdapter,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CacheFSAdapter(ICacheAdapter):
|
|
13
|
+
def __init__(self, cache_dir: str):
|
|
14
|
+
self.cache_dir = cache_dir
|
|
15
|
+
|
|
16
|
+
if not os.path.exists(self.cache_dir):
|
|
17
|
+
os.makedirs(self.cache_dir)
|
|
18
|
+
|
|
19
|
+
def __key_to_sha256(self, key: str) -> str:
|
|
20
|
+
return hashlib.sha256(key.encode()).hexdigest()
|
|
21
|
+
|
|
22
|
+
def get(self, key: str) -> CachedData:
|
|
23
|
+
if not os.path.exists(os.path.join(self.cache_dir, self.__key_to_sha256(key))):
|
|
24
|
+
raise CacheNotFoundError(f"Cache file not found: {key}")
|
|
25
|
+
|
|
26
|
+
with open(os.path.join(self.cache_dir, self.__key_to_sha256(key)), "r") as f:
|
|
27
|
+
return CachedData(**json.load(f))
|
|
28
|
+
|
|
29
|
+
def set(self, key: str, value: CachedData) -> None:
|
|
30
|
+
with open(os.path.join(self.cache_dir, self.__key_to_sha256(key)), "w") as f:
|
|
31
|
+
json.dump(value.model_dump(), f, indent=4)
|
|
32
|
+
|
|
33
|
+
def delete(self, key: str) -> None:
|
|
34
|
+
if not os.path.exists(os.path.join(self.cache_dir, self.__key_to_sha256(key))):
|
|
35
|
+
raise CacheNotFoundError(f"Cache file not found: {key}")
|
|
36
|
+
os.remove(os.path.join(self.cache_dir, self.__key_to_sha256(key)))
|
|
37
|
+
|
|
38
|
+
def exists(self, key: str) -> bool:
|
|
39
|
+
return os.path.exists(os.path.join(self.cache_dir, self.__key_to_sha256(key)))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from naas_abi_core.services.object_storage.adapters.secondary.ObjectStorageSecondaryAdapterFS import (
|
|
2
|
+
ObjectStorageSecondaryAdapterFS,
|
|
3
|
+
)
|
|
4
|
+
from naas_abi_core.services.object_storage.adapters.secondary.ObjectStorageSecondaryAdapterNaas import (
|
|
5
|
+
ObjectStorageSecondaryAdapterNaas,
|
|
6
|
+
)
|
|
7
|
+
from naas_abi_core.services.object_storage.adapters.secondary.ObjectStorageSecondaryAdapterS3 import (
|
|
8
|
+
ObjectStorageSecondaryAdapterS3,
|
|
9
|
+
)
|
|
10
|
+
from naas_abi_core.services.object_storage.ObjectStorageService import (
|
|
11
|
+
ObjectStorageService,
|
|
12
|
+
)
|
|
13
|
+
from naas_abi_core.utils.Storage import find_storage_folder
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ObjectStorageFactory:
|
|
17
|
+
@staticmethod
|
|
18
|
+
def ObjectStorageServiceFS__find_storage(
|
|
19
|
+
needle: str = "storage",
|
|
20
|
+
) -> ObjectStorageService:
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
return ObjectStorageService(
|
|
24
|
+
ObjectStorageSecondaryAdapterFS(find_storage_folder(os.getcwd()))
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def ObjectStorageServiceFS(base_path: str) -> ObjectStorageService:
|
|
29
|
+
return ObjectStorageService(ObjectStorageSecondaryAdapterFS(base_path))
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def ObjectStorageServiceS3(
|
|
33
|
+
access_key_id: str,
|
|
34
|
+
secret_access_key: str,
|
|
35
|
+
bucket_name: str,
|
|
36
|
+
base_prefix: str,
|
|
37
|
+
session_token: str | None = None,
|
|
38
|
+
) -> ObjectStorageService:
|
|
39
|
+
return ObjectStorageService(
|
|
40
|
+
ObjectStorageSecondaryAdapterS3(
|
|
41
|
+
bucket_name,
|
|
42
|
+
access_key_id,
|
|
43
|
+
secret_access_key,
|
|
44
|
+
base_prefix,
|
|
45
|
+
session_token,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def ObjectStorageServiceNaas(
|
|
51
|
+
naas_api_key: str, workspace_id: str, storage_name: str, base_prefix: str = ""
|
|
52
|
+
) -> ObjectStorageService:
|
|
53
|
+
return ObjectStorageService(
|
|
54
|
+
ObjectStorageSecondaryAdapterNaas(
|
|
55
|
+
naas_api_key, workspace_id, storage_name, base_prefix
|
|
56
|
+
)
|
|
57
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Exceptions:
|
|
7
|
+
class ObjectNotFound(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
class ObjectAlreadyExists(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IObjectStorageAdapter(ABC):
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get_object(self, prefix: str, key: str) -> bytes:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def put_object(self, prefix: str, key: str, content: bytes) -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def delete_object(self, prefix: str, key: str) -> None:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def list_objects(self, prefix: str, queue: Optional[Queue] = None) -> list[str]:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IObjectStorageDomain(ABC):
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def get_object(self, prefix: str, key: str) -> bytes:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def put_object(self, prefix: str, key: str, content: bytes) -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def delete_object(self, prefix: str, key: str) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def list_objects(self, prefix: str, queue: Optional[Queue] = None) -> list[str]:
|
|
47
|
+
pass
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from queue import Queue
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from naas_abi_core.services.object_storage.ObjectStoragePort import (
|
|
5
|
+
IObjectStorageAdapter,
|
|
6
|
+
IObjectStorageDomain,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ObjectStorageService(IObjectStorageDomain):
|
|
11
|
+
adapter: IObjectStorageAdapter
|
|
12
|
+
|
|
13
|
+
def __init__(self, adapter: IObjectStorageAdapter):
|
|
14
|
+
self.adapter = adapter
|
|
15
|
+
|
|
16
|
+
# Function to avoid creating a new folder 'storage' while using FS adapter
|
|
17
|
+
def __remove_storage_prefix(self, prefix: str) -> str:
|
|
18
|
+
if prefix.startswith("storage/"):
|
|
19
|
+
return prefix.replace("storage/", "")
|
|
20
|
+
return prefix
|
|
21
|
+
|
|
22
|
+
def get_object(self, prefix: str, key: str) -> bytes:
|
|
23
|
+
prefix = self.__remove_storage_prefix(prefix)
|
|
24
|
+
return self.adapter.get_object(prefix, key)
|
|
25
|
+
|
|
26
|
+
def put_object(self, prefix: str, key: str, content: bytes) -> None:
|
|
27
|
+
prefix = self.__remove_storage_prefix(prefix)
|
|
28
|
+
self.adapter.put_object(prefix, key, content)
|
|
29
|
+
|
|
30
|
+
def delete_object(self, prefix: str, key: str) -> None:
|
|
31
|
+
prefix = self.__remove_storage_prefix(prefix)
|
|
32
|
+
self.adapter.delete_object(prefix, key)
|
|
33
|
+
|
|
34
|
+
def list_objects(
|
|
35
|
+
self, prefix: str = "", queue: Optional[Queue] = None
|
|
36
|
+
) -> list[str]:
|
|
37
|
+
prefix = self.__remove_storage_prefix(prefix)
|
|
38
|
+
if prefix == "/":
|
|
39
|
+
prefix = ""
|
|
40
|
+
|
|
41
|
+
return self.adapter.list_objects(prefix, queue)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from naas_abi_core.services.object_storage.ObjectStoragePort import (
|
|
6
|
+
Exceptions,
|
|
7
|
+
IObjectStorageAdapter,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ObjectStorageSecondaryAdapterFS(IObjectStorageAdapter):
|
|
12
|
+
def __init__(self, base_path: str):
|
|
13
|
+
self.base_path = base_path
|
|
14
|
+
self.__create_path(base_path)
|
|
15
|
+
|
|
16
|
+
def __create_path(self, prefix: str) -> None:
|
|
17
|
+
os.makedirs(os.path.join(self.base_path, prefix), exist_ok=True)
|
|
18
|
+
|
|
19
|
+
def __path_exists(self, prefix: str, key: str | None = None) -> bool:
|
|
20
|
+
if key is None:
|
|
21
|
+
exists = os.path.exists(os.path.join(self.base_path, prefix))
|
|
22
|
+
else:
|
|
23
|
+
exists = os.path.exists(os.path.join(self.base_path, prefix, key))
|
|
24
|
+
|
|
25
|
+
if not exists:
|
|
26
|
+
raise Exceptions.ObjectNotFound(f"Object {prefix}/{key} not found")
|
|
27
|
+
|
|
28
|
+
return exists
|
|
29
|
+
|
|
30
|
+
def get_object(self, prefix: str, key: str) -> bytes:
|
|
31
|
+
self.__path_exists(prefix, key)
|
|
32
|
+
|
|
33
|
+
with open(os.path.join(self.base_path, prefix, key), "rb") as f:
|
|
34
|
+
return f.read()
|
|
35
|
+
|
|
36
|
+
def put_object(self, prefix: str, key: str, content: bytes) -> None:
|
|
37
|
+
self.__create_path(prefix)
|
|
38
|
+
|
|
39
|
+
with open(os.path.join(self.base_path, prefix, key), "wb") as f:
|
|
40
|
+
f.write(content)
|
|
41
|
+
|
|
42
|
+
def delete_object(self, prefix: str, key: str) -> None:
|
|
43
|
+
self.__path_exists(prefix, key)
|
|
44
|
+
|
|
45
|
+
os.remove(os.path.join(self.base_path, prefix, key))
|
|
46
|
+
|
|
47
|
+
def list_objects(self, prefix: str, queue: Optional[Queue] = None) -> list[str]:
|
|
48
|
+
self.__path_exists(prefix)
|
|
49
|
+
return [
|
|
50
|
+
os.path.join(prefix, f)
|
|
51
|
+
for f in os.listdir(os.path.join(self.base_path, prefix))
|
|
52
|
+
]
|