solace-agent-mesh 0.0.1__py3-none-any.whl → 0.1.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.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/__init__.py +0 -3
- solace_agent_mesh/agents/__init__.py +0 -0
- solace_agent_mesh/agents/base_agent_component.py +224 -0
- solace_agent_mesh/agents/global/__init__.py +0 -0
- solace_agent_mesh/agents/global/actions/__init__.py +0 -0
- solace_agent_mesh/agents/global/actions/agent_state_change.py +54 -0
- solace_agent_mesh/agents/global/actions/clear_history.py +32 -0
- solace_agent_mesh/agents/global/actions/convert_file_to_markdown.py +160 -0
- solace_agent_mesh/agents/global/actions/create_file.py +70 -0
- solace_agent_mesh/agents/global/actions/error_action.py +45 -0
- solace_agent_mesh/agents/global/actions/plantuml_diagram.py +93 -0
- solace_agent_mesh/agents/global/actions/plotly_graph.py +117 -0
- solace_agent_mesh/agents/global/actions/retrieve_file.py +51 -0
- solace_agent_mesh/agents/global/global_agent_component.py +38 -0
- solace_agent_mesh/agents/image_processing/__init__.py +0 -0
- solace_agent_mesh/agents/image_processing/actions/__init__.py +0 -0
- solace_agent_mesh/agents/image_processing/actions/create_image.py +75 -0
- solace_agent_mesh/agents/image_processing/actions/describe_image.py +115 -0
- solace_agent_mesh/agents/image_processing/image_processing_agent_component.py +23 -0
- solace_agent_mesh/agents/slack/__init__.py +1 -0
- solace_agent_mesh/agents/slack/actions/__init__.py +1 -0
- solace_agent_mesh/agents/slack/actions/post_message.py +177 -0
- solace_agent_mesh/agents/slack/slack_agent_component.py +59 -0
- solace_agent_mesh/agents/web_request/__init__.py +0 -0
- solace_agent_mesh/agents/web_request/actions/__init__.py +0 -0
- solace_agent_mesh/agents/web_request/actions/do_image_search.py +84 -0
- solace_agent_mesh/agents/web_request/actions/do_news_search.py +47 -0
- solace_agent_mesh/agents/web_request/actions/do_suggestion_search.py +34 -0
- solace_agent_mesh/agents/web_request/actions/do_web_request.py +134 -0
- solace_agent_mesh/agents/web_request/actions/download_file.py +69 -0
- solace_agent_mesh/agents/web_request/web_request_agent_component.py +33 -0
- solace_agent_mesh/assets/web-visualizer/assets/index-C5awueeJ.js +109 -0
- solace_agent_mesh/assets/web-visualizer/assets/index-D0qORgkg.css +1 -0
- solace_agent_mesh/assets/web-visualizer/index.html +14 -0
- solace_agent_mesh/assets/web-visualizer/vite.svg +1 -0
- solace_agent_mesh/cli/__init__.py +1 -0
- solace_agent_mesh/cli/commands/__init__.py +0 -0
- solace_agent_mesh/cli/commands/add/__init__.py +3 -0
- solace_agent_mesh/cli/commands/add/add.py +88 -0
- solace_agent_mesh/cli/commands/add/agent.py +110 -0
- solace_agent_mesh/cli/commands/add/copy_from_plugin.py +90 -0
- solace_agent_mesh/cli/commands/add/gateway.py +221 -0
- solace_agent_mesh/cli/commands/build.py +631 -0
- solace_agent_mesh/cli/commands/chat/__init__.py +3 -0
- solace_agent_mesh/cli/commands/chat/chat.py +361 -0
- solace_agent_mesh/cli/commands/config.py +29 -0
- solace_agent_mesh/cli/commands/init/__init__.py +3 -0
- solace_agent_mesh/cli/commands/init/ai_provider_step.py +76 -0
- solace_agent_mesh/cli/commands/init/broker_step.py +102 -0
- solace_agent_mesh/cli/commands/init/builtin_agent_step.py +88 -0
- solace_agent_mesh/cli/commands/init/check_if_already_done.py +13 -0
- solace_agent_mesh/cli/commands/init/create_config_file_step.py +52 -0
- solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +96 -0
- solace_agent_mesh/cli/commands/init/file_service_step.py +73 -0
- solace_agent_mesh/cli/commands/init/init.py +114 -0
- solace_agent_mesh/cli/commands/init/project_structure_step.py +45 -0
- solace_agent_mesh/cli/commands/init/rest_api_step.py +50 -0
- solace_agent_mesh/cli/commands/init/web_ui_step.py +40 -0
- solace_agent_mesh/cli/commands/plugin/__init__.py +3 -0
- solace_agent_mesh/cli/commands/plugin/add.py +98 -0
- solace_agent_mesh/cli/commands/plugin/build.py +217 -0
- solace_agent_mesh/cli/commands/plugin/create.py +117 -0
- solace_agent_mesh/cli/commands/plugin/plugin.py +109 -0
- solace_agent_mesh/cli/commands/plugin/remove.py +71 -0
- solace_agent_mesh/cli/commands/run.py +68 -0
- solace_agent_mesh/cli/commands/visualizer.py +138 -0
- solace_agent_mesh/cli/config.py +81 -0
- solace_agent_mesh/cli/main.py +306 -0
- solace_agent_mesh/cli/utils.py +246 -0
- solace_agent_mesh/common/__init__.py +0 -0
- solace_agent_mesh/common/action.py +91 -0
- solace_agent_mesh/common/action_list.py +37 -0
- solace_agent_mesh/common/action_response.py +327 -0
- solace_agent_mesh/common/constants.py +3 -0
- solace_agent_mesh/common/mysql_database.py +40 -0
- solace_agent_mesh/common/postgres_database.py +79 -0
- solace_agent_mesh/common/prompt_templates.py +30 -0
- solace_agent_mesh/common/prompt_templates_unused_delete.py +161 -0
- solace_agent_mesh/common/stimulus_utils.py +152 -0
- solace_agent_mesh/common/time.py +24 -0
- solace_agent_mesh/common/utils.py +638 -0
- solace_agent_mesh/configs/agent_global.yaml +74 -0
- solace_agent_mesh/configs/agent_image_processing.yaml +82 -0
- solace_agent_mesh/configs/agent_slack.yaml +64 -0
- solace_agent_mesh/configs/agent_web_request.yaml +75 -0
- solace_agent_mesh/configs/conversation_to_file.yaml +56 -0
- solace_agent_mesh/configs/error_catcher.yaml +56 -0
- solace_agent_mesh/configs/monitor.yaml +0 -0
- solace_agent_mesh/configs/monitor_stim_and_errors_to_slack.yaml +106 -0
- solace_agent_mesh/configs/monitor_user_feedback.yaml +58 -0
- solace_agent_mesh/configs/orchestrator.yaml +241 -0
- solace_agent_mesh/configs/service_embedding.yaml +81 -0
- solace_agent_mesh/configs/service_llm.yaml +265 -0
- solace_agent_mesh/configs/visualize_websocket.yaml +55 -0
- solace_agent_mesh/gateway/__init__.py +0 -0
- solace_agent_mesh/gateway/components/__init__.py +0 -0
- solace_agent_mesh/gateway/components/gateway_base.py +41 -0
- solace_agent_mesh/gateway/components/gateway_input.py +265 -0
- solace_agent_mesh/gateway/components/gateway_output.py +289 -0
- solace_agent_mesh/gateway/identity/bamboohr_identity.py +18 -0
- solace_agent_mesh/gateway/identity/identity_base.py +10 -0
- solace_agent_mesh/gateway/identity/identity_provider.py +60 -0
- solace_agent_mesh/gateway/identity/no_identity.py +9 -0
- solace_agent_mesh/gateway/identity/passthru_identity.py +9 -0
- solace_agent_mesh/monitors/base_monitor_component.py +26 -0
- solace_agent_mesh/monitors/feedback/user_feedback_monitor.py +75 -0
- solace_agent_mesh/monitors/stim_and_errors/stim_and_error_monitor.py +560 -0
- solace_agent_mesh/orchestrator/__init__.py +0 -0
- solace_agent_mesh/orchestrator/action_manager.py +225 -0
- solace_agent_mesh/orchestrator/components/__init__.py +0 -0
- solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +54 -0
- solace_agent_mesh/orchestrator/components/orchestrator_action_response_component.py +179 -0
- solace_agent_mesh/orchestrator/components/orchestrator_register_component.py +107 -0
- solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +477 -0
- solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +246 -0
- solace_agent_mesh/orchestrator/orchestrator_main.py +166 -0
- solace_agent_mesh/orchestrator/orchestrator_prompt.py +410 -0
- solace_agent_mesh/services/__init__.py +0 -0
- solace_agent_mesh/services/authorization/providers/base_authorization_provider.py +56 -0
- solace_agent_mesh/services/bamboo_hr_service/__init__.py +3 -0
- solace_agent_mesh/services/bamboo_hr_service/bamboo_hr.py +182 -0
- solace_agent_mesh/services/common/__init__.py +4 -0
- solace_agent_mesh/services/common/auto_expiry.py +45 -0
- solace_agent_mesh/services/common/singleton.py +18 -0
- solace_agent_mesh/services/file_service/__init__.py +14 -0
- solace_agent_mesh/services/file_service/file_manager/__init__.py +0 -0
- solace_agent_mesh/services/file_service/file_manager/bucket_file_manager.py +149 -0
- solace_agent_mesh/services/file_service/file_manager/file_manager_base.py +162 -0
- solace_agent_mesh/services/file_service/file_manager/memory_file_manager.py +64 -0
- solace_agent_mesh/services/file_service/file_manager/volume_file_manager.py +106 -0
- solace_agent_mesh/services/file_service/file_service.py +432 -0
- solace_agent_mesh/services/file_service/file_service_constants.py +54 -0
- solace_agent_mesh/services/file_service/file_transformations.py +131 -0
- solace_agent_mesh/services/file_service/file_utils.py +322 -0
- solace_agent_mesh/services/file_service/transformers/__init__.py +5 -0
- solace_agent_mesh/services/history_service/__init__.py +3 -0
- solace_agent_mesh/services/history_service/history_providers/__init__.py +0 -0
- solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +78 -0
- solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +167 -0
- solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +163 -0
- solace_agent_mesh/services/history_service/history_service.py +139 -0
- solace_agent_mesh/services/llm_service/components/llm_request_component.py +293 -0
- solace_agent_mesh/services/llm_service/components/llm_service_component_base.py +152 -0
- solace_agent_mesh/services/middleware_service/__init__.py +0 -0
- solace_agent_mesh/services/middleware_service/middleware_service.py +20 -0
- solace_agent_mesh/templates/action.py +38 -0
- solace_agent_mesh/templates/agent.py +29 -0
- solace_agent_mesh/templates/agent.yaml +70 -0
- solace_agent_mesh/templates/gateway-config-template.yaml +6 -0
- solace_agent_mesh/templates/gateway-default-config.yaml +28 -0
- solace_agent_mesh/templates/gateway-flows.yaml +81 -0
- solace_agent_mesh/templates/gateway-header.yaml +16 -0
- solace_agent_mesh/templates/gateway_base.py +15 -0
- solace_agent_mesh/templates/gateway_input.py +98 -0
- solace_agent_mesh/templates/gateway_output.py +71 -0
- solace_agent_mesh/templates/plugin-pyproject.toml +30 -0
- solace_agent_mesh/templates/rest-api-default-config.yaml +24 -0
- solace_agent_mesh/templates/rest-api-flows.yaml +80 -0
- solace_agent_mesh/templates/slack-default-config.yaml +9 -0
- solace_agent_mesh/templates/slack-flows.yaml +90 -0
- solace_agent_mesh/templates/solace-agent-mesh-default.yaml +77 -0
- solace_agent_mesh/templates/solace-agent-mesh-plugin-default.yaml +8 -0
- solace_agent_mesh/templates/web-default-config.yaml +5 -0
- solace_agent_mesh/templates/web-flows.yaml +86 -0
- solace_agent_mesh/tools/__init__.py +0 -0
- solace_agent_mesh/tools/components/__init__.py +0 -0
- solace_agent_mesh/tools/components/conversation_formatter.py +111 -0
- solace_agent_mesh/tools/components/file_resolver_component.py +58 -0
- solace_agent_mesh/tools/config/runtime_config.py +26 -0
- solace_agent_mesh-0.1.1.dist-info/METADATA +179 -0
- solace_agent_mesh-0.1.1.dist-info/RECORD +174 -0
- solace_agent_mesh-0.1.1.dist-info/entry_points.txt +3 -0
- solace_agent_mesh-0.0.1.dist-info/licenses/LICENSE.txt → solace_agent_mesh-0.1.1.dist-info/licenses/LICENSE +1 -2
- solace_agent_mesh-0.0.1.dist-info/METADATA +0 -51
- solace_agent_mesh-0.0.1.dist-info/RECORD +0 -5
- {solace_agent_mesh-0.0.1.dist-info → solace_agent_mesh-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from solace_ai_connector.common.log import log
|
|
6
|
+
|
|
7
|
+
class AutoExpiry(ABC):
|
|
8
|
+
_expiry_thread = None
|
|
9
|
+
expiration_check_interval = None
|
|
10
|
+
_stop_expiry_thread = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _start_auto_expiry_thread(self, expiration_check_interval):
|
|
14
|
+
"""Starts a background thread to handle auto-expiry."""
|
|
15
|
+
self.expiration_check_interval = expiration_check_interval
|
|
16
|
+
self._stop_expiry_thread = threading.Event()
|
|
17
|
+
self._expiry_thread = threading.Thread(
|
|
18
|
+
target=self._auto_expiry_task, daemon=True
|
|
19
|
+
)
|
|
20
|
+
self._expiry_thread.start()
|
|
21
|
+
|
|
22
|
+
def _auto_expiry_task(self):
|
|
23
|
+
"""Background task to check and delete expired items."""
|
|
24
|
+
while not self._stop_expiry_thread.is_set():
|
|
25
|
+
try:
|
|
26
|
+
self._delete_expired_items()
|
|
27
|
+
except Exception as e:
|
|
28
|
+
log.error(f"Error during auto-expiry process: {e}")
|
|
29
|
+
time.sleep(self.expiration_check_interval)
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def _delete_expired_items(self):
|
|
33
|
+
"""Checks all item and deletes those that have exceeded max_time_to_live."""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
def stop_auto_expiry(self):
|
|
37
|
+
"""Stops the auto-expiry background thread."""
|
|
38
|
+
if self._stop_expiry_thread:
|
|
39
|
+
self._stop_expiry_thread.set()
|
|
40
|
+
if self._expiry_thread and self._expiry_thread.is_alive():
|
|
41
|
+
self._expiry_thread.join()
|
|
42
|
+
|
|
43
|
+
def __del__(self):
|
|
44
|
+
"""Ensure the expiry thread stops when the service is destroyed."""
|
|
45
|
+
self.stop_auto_expiry()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .auto_expiry import AutoExpiry
|
|
2
|
+
|
|
3
|
+
# Singleton pattern - Ensuring that only one instance of the class is created per given identifier
|
|
4
|
+
class SingletonMeta(type):
|
|
5
|
+
_instances = {}
|
|
6
|
+
|
|
7
|
+
def __call__(cls, *args, **kwargs):
|
|
8
|
+
identifier = kwargs.get("identifier", "default")
|
|
9
|
+
if cls not in cls._instances:
|
|
10
|
+
cls._instances[cls] = {}
|
|
11
|
+
if identifier not in cls._instances[cls]:
|
|
12
|
+
instance = super().__call__(*args, **kwargs)
|
|
13
|
+
cls._instances[cls][identifier] = instance
|
|
14
|
+
return cls._instances[cls][identifier]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AutoExpirySingletonMeta(SingletonMeta, type(AutoExpiry)):
|
|
18
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .file_service import FileService, FileServicePermissionError, FS_URL_REGEX
|
|
2
|
+
from .file_utils import Types
|
|
3
|
+
from .file_transformations import LLM_QUERY_OPTIONS, TRANSFORMERS
|
|
4
|
+
from .file_service_constants import FS_PROTOCOL
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"FileService",
|
|
8
|
+
"FS_PROTOCOL",
|
|
9
|
+
"Types",
|
|
10
|
+
"FileServicePermissionError",
|
|
11
|
+
"FS_URL_REGEX",
|
|
12
|
+
"LLM_QUERY_OPTIONS",
|
|
13
|
+
"TRANSFORMERS"
|
|
14
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from botocore.exceptions import NoCredentialsError, ClientError
|
|
6
|
+
|
|
7
|
+
from .file_manager_base import FileManagerBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BucketFileManager(FileManagerBase):
|
|
11
|
+
def __init__(self, config, ttl):
|
|
12
|
+
self.config = config
|
|
13
|
+
self.ttl = ttl
|
|
14
|
+
self.bucket_name = config.get("bucket_name")
|
|
15
|
+
self.boto3_config = config.get("boto3_config", {})
|
|
16
|
+
self.endpoint_url = config.get("endpoint_url", None)
|
|
17
|
+
session = boto3.Session(**self.boto3_config)
|
|
18
|
+
s3_resource = session.resource("s3", endpoint_url=self.endpoint_url)
|
|
19
|
+
try:
|
|
20
|
+
if not self.bucket_name:
|
|
21
|
+
raise Exception("Bucket name not provided.")
|
|
22
|
+
|
|
23
|
+
bucket = s3_resource.Bucket(self.bucket_name)
|
|
24
|
+
|
|
25
|
+
# Check if the bucket exists by attempting to load it
|
|
26
|
+
bucket.load()
|
|
27
|
+
self.bucket = bucket
|
|
28
|
+
|
|
29
|
+
except ClientError as e:
|
|
30
|
+
# Handle the case where the bucket doesn't exist or access is denied
|
|
31
|
+
error_code = e.response["Error"]["Code"]
|
|
32
|
+
if error_code == "404":
|
|
33
|
+
raise Exception((f"Bucket '{self.bucket_name}' does not exist."), e)
|
|
34
|
+
elif error_code == "403":
|
|
35
|
+
raise Exception(
|
|
36
|
+
(f"Access to bucket '{self.bucket_name}' is denied."), e
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
raise Exception(f"Unexpected error: {e}")
|
|
40
|
+
|
|
41
|
+
def _save_metadata(self, file_signature: str, metadata: dict):
|
|
42
|
+
metadata_key = self._get_metadata_name(file_signature)
|
|
43
|
+
metadata_content = json.dumps(metadata, indent=4)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
self.bucket.put_object(
|
|
47
|
+
Key=metadata_key,
|
|
48
|
+
Body=metadata_content,
|
|
49
|
+
ContentType="application/json",
|
|
50
|
+
)
|
|
51
|
+
except (NoCredentialsError, ClientError) as e:
|
|
52
|
+
raise RuntimeError(f"Failed to save metadata to S3: {str(e)}")
|
|
53
|
+
|
|
54
|
+
def upload_from_buffer(self, buffer: bytes, file_name: str, **kwargs) -> dict:
|
|
55
|
+
file_signature = self._generate_file_signature(file_name)
|
|
56
|
+
metadata = self._create_metadata(file_signature, file_name, buffer, kwargs)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
self.bucket.put_object(
|
|
60
|
+
Key=file_signature,
|
|
61
|
+
Body=buffer,
|
|
62
|
+
ContentType=metadata.get("mime_type") or "application/octet-stream",
|
|
63
|
+
)
|
|
64
|
+
except (NoCredentialsError, ClientError) as e:
|
|
65
|
+
raise RuntimeError(f"Failed to upload file to S3: {str(e)}")
|
|
66
|
+
|
|
67
|
+
self._save_metadata(file_signature, metadata)
|
|
68
|
+
|
|
69
|
+
return metadata
|
|
70
|
+
|
|
71
|
+
def upload_from_file(self, file_path: str, **kwargs) -> dict:
|
|
72
|
+
if not os.path.isfile(file_path):
|
|
73
|
+
raise FileNotFoundError(f"The file {file_path} does not exist.")
|
|
74
|
+
|
|
75
|
+
file_name = os.path.basename(file_path)
|
|
76
|
+
file_signature = self._generate_file_signature(file_name)
|
|
77
|
+
content_type = kwargs.get("mime_type") or self._get_mime_type(file_name) or "application/octet-stream"
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
with open(file_path, "rb") as file_data:
|
|
81
|
+
self.bucket.put_object(
|
|
82
|
+
Key=file_signature,
|
|
83
|
+
Body=file_data,
|
|
84
|
+
ContentType=content_type,
|
|
85
|
+
)
|
|
86
|
+
metadata = self._create_metadata(file_signature, file_name, file_data.read(), kwargs)
|
|
87
|
+
except (NoCredentialsError, ClientError) as e:
|
|
88
|
+
raise RuntimeError(f"Failed to upload file to S3: {str(e)}")
|
|
89
|
+
|
|
90
|
+
self._save_metadata(file_signature, metadata)
|
|
91
|
+
|
|
92
|
+
return metadata
|
|
93
|
+
|
|
94
|
+
def download_to_buffer(self, file_name: str) -> bytes:
|
|
95
|
+
try:
|
|
96
|
+
buffer = BytesIO()
|
|
97
|
+
obj = self.bucket.Object(file_name)
|
|
98
|
+
obj.download_fileobj(buffer)
|
|
99
|
+
buffer.seek(0)
|
|
100
|
+
return buffer.read()
|
|
101
|
+
except (NoCredentialsError, ClientError) as e:
|
|
102
|
+
raise RuntimeError(f"Failed to download file from S3: {str(e)}")
|
|
103
|
+
|
|
104
|
+
def download_to_file(self, file_name: str, destination_path: str):
|
|
105
|
+
buffer = self.download_to_buffer(file_name)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
with open(destination_path, "wb") as destination_file:
|
|
109
|
+
destination_file.write(buffer)
|
|
110
|
+
except IOError as e:
|
|
111
|
+
raise RuntimeError(f"Failed to write file to destination: {str(e)}")
|
|
112
|
+
|
|
113
|
+
def get_metadata(self, file_name: str) -> dict:
|
|
114
|
+
metadata_key = self._get_metadata_name(file_name)
|
|
115
|
+
meta_buffer = self.download_to_buffer(metadata_key)
|
|
116
|
+
return json.loads(meta_buffer)
|
|
117
|
+
|
|
118
|
+
def delete_by_name(self, file_name: str):
|
|
119
|
+
metadata_key = self._get_metadata_name(file_name)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# the main file
|
|
123
|
+
file = self.bucket.Object(file_name)
|
|
124
|
+
# the metadata file
|
|
125
|
+
meta = self.bucket.Object(metadata_key)
|
|
126
|
+
|
|
127
|
+
# Delete the files
|
|
128
|
+
file.delete()
|
|
129
|
+
meta.delete()
|
|
130
|
+
except (NoCredentialsError, ClientError) as e:
|
|
131
|
+
raise RuntimeError(f"Failed to delete file from S3: {str(e)}")
|
|
132
|
+
|
|
133
|
+
def update_file_expiration(self, file_signature, expiration_timestamp):
|
|
134
|
+
metadata = self.get_metadata(file_signature)
|
|
135
|
+
metadata["expiration_timestamp"] = expiration_timestamp
|
|
136
|
+
self._save_metadata(file_signature, metadata)
|
|
137
|
+
|
|
138
|
+
def list_all_metadata(self) -> list:
|
|
139
|
+
all_metadata = []
|
|
140
|
+
try:
|
|
141
|
+
for obj in self.bucket.objects.all():
|
|
142
|
+
if obj.key.endswith(".metadata"):
|
|
143
|
+
meta_buffer = self.download_to_buffer(obj.key)
|
|
144
|
+
metadata = json.loads(meta_buffer)
|
|
145
|
+
|
|
146
|
+
all_metadata.append(metadata)
|
|
147
|
+
except (NoCredentialsError, ClientError) as e:
|
|
148
|
+
raise RuntimeError(f"Failed to list metadata from S3: {str(e)}")
|
|
149
|
+
return all_metadata
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import mimetypes
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from ..file_service_constants import FS_PROTOCOL, META_FILE_EXTENSION
|
|
8
|
+
from ..file_utils import get_file_schema_and_shape
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MAX_NAME_LENGTH = 255 - (
|
|
12
|
+
len(META_FILE_EXTENSION) + len(FS_PROTOCOL) + 3 + 36 + 1
|
|
13
|
+
) # 36 is the length of a UUID, 3 is ://
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileManagerBase(ABC):
|
|
17
|
+
ttl: int
|
|
18
|
+
|
|
19
|
+
def _generate_file_signature(self, file_name: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Generate a file signature from the original file name.
|
|
22
|
+
"""
|
|
23
|
+
file_signature = str(uuid.uuid4())
|
|
24
|
+
# Remove any leading or trailing whitespace
|
|
25
|
+
file_name = file_name.strip()
|
|
26
|
+
# Replace invalid characters and space/ampersand with underscores
|
|
27
|
+
cleaned_name = re.sub(r'[<>:"/\\|?*& ]', "_", file_name)
|
|
28
|
+
# replace unicode and binary characters with underscores
|
|
29
|
+
cleaned_name = re.sub(r"[^\x00-\x7F]+", "_", cleaned_name)
|
|
30
|
+
cleaned_name = re.sub(r"\\u[0-9a-fA-F]{4}", "_", cleaned_name)
|
|
31
|
+
# Replace multiple underscores with a single underscore
|
|
32
|
+
cleaned_name = re.sub(r"_+", "_", cleaned_name)
|
|
33
|
+
# Truncate the signature if it is too long
|
|
34
|
+
if len(cleaned_name) > MAX_NAME_LENGTH:
|
|
35
|
+
start_index = len(cleaned_name) - MAX_NAME_LENGTH
|
|
36
|
+
# Keep the latter portion of the name
|
|
37
|
+
cleaned_name = cleaned_name[start_index:]
|
|
38
|
+
signature = f"{file_signature}_{cleaned_name}"
|
|
39
|
+
return signature
|
|
40
|
+
|
|
41
|
+
def _get_url_from_signature(self, file_signature: str) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Generate a file URL from a file signature.
|
|
44
|
+
"""
|
|
45
|
+
return f"{FS_PROTOCOL}://{file_signature}"
|
|
46
|
+
|
|
47
|
+
def _get_mime_type(self, file_name: str) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Get the MIME type of a file.
|
|
50
|
+
"""
|
|
51
|
+
mime_type, _ = mimetypes.guess_type(file_name)
|
|
52
|
+
return mime_type
|
|
53
|
+
|
|
54
|
+
def _get_metadata_name(self, name: str) -> dict:
|
|
55
|
+
"""
|
|
56
|
+
Get metadata from a file URL.
|
|
57
|
+
"""
|
|
58
|
+
metadata_path = f"{name}{META_FILE_EXTENSION}"
|
|
59
|
+
return metadata_path
|
|
60
|
+
|
|
61
|
+
def _create_metadata(self, file_signature: str, file_name: str, file: bytes, metadata: dict):
|
|
62
|
+
"""
|
|
63
|
+
Extend metadata with schema and shape if not present
|
|
64
|
+
Add file_size if not present
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
mime_type = self._get_mime_type(file_name)
|
|
68
|
+
file_url = self._get_url_from_signature(file_signature)
|
|
69
|
+
|
|
70
|
+
if not isinstance(metadata, dict):
|
|
71
|
+
metadata = {}
|
|
72
|
+
meta_copy = metadata.copy()
|
|
73
|
+
|
|
74
|
+
# Can not be provided by user
|
|
75
|
+
meta_copy["url"] = file_url
|
|
76
|
+
|
|
77
|
+
if "mime_type" not in meta_copy:
|
|
78
|
+
meta_copy["mime_type"] = mime_type
|
|
79
|
+
|
|
80
|
+
if "name" not in meta_copy:
|
|
81
|
+
meta_copy["name"] = file_name
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
"schema-yaml" not in meta_copy
|
|
85
|
+
or "schema_yaml" not in meta_copy
|
|
86
|
+
or "shape" not in meta_copy
|
|
87
|
+
):
|
|
88
|
+
schema, shape = get_file_schema_and_shape(file, meta_copy)
|
|
89
|
+
if "schema-yaml" not in meta_copy and schema:
|
|
90
|
+
meta_copy["schema-yaml"] = schema
|
|
91
|
+
|
|
92
|
+
if "shape" not in meta_copy and shape:
|
|
93
|
+
meta_copy["shape"] = shape
|
|
94
|
+
|
|
95
|
+
if "file_size" not in meta_copy:
|
|
96
|
+
meta_copy["file_size"] = len(file)
|
|
97
|
+
|
|
98
|
+
if "upload_timestamp" not in meta_copy:
|
|
99
|
+
meta_copy["upload_timestamp"] = time.time()
|
|
100
|
+
|
|
101
|
+
if "expiration_timestamp" not in meta_copy:
|
|
102
|
+
meta_copy["expiration_timestamp"] = meta_copy["upload_timestamp"] + self.ttl
|
|
103
|
+
|
|
104
|
+
return meta_copy
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def update_file_expiration(self, file_signature: str, expiration_timestamp: float):
|
|
108
|
+
"""
|
|
109
|
+
Update the expiration timestamp for a file.
|
|
110
|
+
"""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def upload_from_buffer(self, buffer: bytes, file_name: str, **kwargs) -> dict:
|
|
115
|
+
"""
|
|
116
|
+
Upload a file from a buffer.
|
|
117
|
+
kwargs are added to metadata
|
|
118
|
+
"""
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
def upload_from_file(self, file_path: str, **kwargs) -> dict:
|
|
123
|
+
"""
|
|
124
|
+
Upload a file from a file path.
|
|
125
|
+
kwargs are added to metadata
|
|
126
|
+
"""
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def download_to_buffer(self, file_name: str) -> bytes:
|
|
131
|
+
"""
|
|
132
|
+
Download a file to a buffer.
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def download_to_file(self, file_name: str, destination_path: str):
|
|
138
|
+
"""
|
|
139
|
+
Download a file to a destination path.
|
|
140
|
+
"""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def delete_by_name(self, file_name: str):
|
|
145
|
+
"""
|
|
146
|
+
Delete a file by name.
|
|
147
|
+
"""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
@abstractmethod
|
|
151
|
+
def get_metadata(self, file_name: str) -> dict:
|
|
152
|
+
"""
|
|
153
|
+
Get metadata for a file.
|
|
154
|
+
"""
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
@abstractmethod
|
|
158
|
+
def list_all_metadata(self) -> list:
|
|
159
|
+
"""
|
|
160
|
+
List all file metadata in the storage.
|
|
161
|
+
"""
|
|
162
|
+
pass
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .file_manager_base import FileManagerBase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MemoryFileManager(FileManagerBase):
|
|
7
|
+
storage = {}
|
|
8
|
+
|
|
9
|
+
def __init__(self, config, ttl):
|
|
10
|
+
self.config = config
|
|
11
|
+
self.ttl = ttl
|
|
12
|
+
|
|
13
|
+
def upload_from_buffer(self, buffer: bytes, file_name: str, **kwargs) -> dict:
|
|
14
|
+
file_signature = self._generate_file_signature(file_name)
|
|
15
|
+
metadata = self._create_metadata(
|
|
16
|
+
file_signature, file_name, buffer, kwargs
|
|
17
|
+
)
|
|
18
|
+
metadata_name = self._get_metadata_name(file_signature)
|
|
19
|
+
self.storage[file_signature] = buffer
|
|
20
|
+
self.storage[metadata_name] = metadata
|
|
21
|
+
return metadata
|
|
22
|
+
|
|
23
|
+
def upload_from_file(self, file_path: str, **kwargs) -> dict:
|
|
24
|
+
"""
|
|
25
|
+
Upload a file from a file path.
|
|
26
|
+
kwargs are added to metadata
|
|
27
|
+
"""
|
|
28
|
+
with open(file_path, "rb") as file:
|
|
29
|
+
buffer = file.read()
|
|
30
|
+
return self.upload_from_buffer(buffer, os.path.basename(file_path), **kwargs)
|
|
31
|
+
|
|
32
|
+
def download_to_buffer(self, file_name: str) -> bytes:
|
|
33
|
+
if not file_name in self.storage:
|
|
34
|
+
raise FileNotFoundError(f"The file {file_name} does not exist.")
|
|
35
|
+
return self.storage[file_name]
|
|
36
|
+
|
|
37
|
+
def download_to_file(self, file_name: str, destination_path: str):
|
|
38
|
+
with open(destination_path, "wb") as file:
|
|
39
|
+
file.write(self.download_to_buffer(file_name))
|
|
40
|
+
|
|
41
|
+
def delete_by_name(self, file_name: str):
|
|
42
|
+
metadata_name = self._get_metadata_name(file_name)
|
|
43
|
+
if file_name in self.storage:
|
|
44
|
+
del self.storage[file_name]
|
|
45
|
+
if metadata_name in self.storage:
|
|
46
|
+
del self.storage[metadata_name]
|
|
47
|
+
|
|
48
|
+
def get_metadata(self, file_name: str) -> dict:
|
|
49
|
+
metadata_name = self._get_metadata_name(file_name)
|
|
50
|
+
if metadata_name in self.storage:
|
|
51
|
+
return self.storage[metadata_name]
|
|
52
|
+
raise FileNotFoundError(f"The file {file_name} does not exist.")
|
|
53
|
+
|
|
54
|
+
def update_file_expiration(self, file_signature, expiration_timestamp):
|
|
55
|
+
metadata_name = self._get_metadata_name(file_signature)
|
|
56
|
+
if metadata_name in self.storage:
|
|
57
|
+
metadata = self.storage[metadata_name]
|
|
58
|
+
metadata["expiration_timestamp"] = expiration_timestamp
|
|
59
|
+
self.storage[metadata_name] = metadata
|
|
60
|
+
else:
|
|
61
|
+
raise FileNotFoundError(f"The file {file_signature} does not exist.")
|
|
62
|
+
|
|
63
|
+
def list_all_metadata(self) -> list:
|
|
64
|
+
return [data for key, data in self.storage.items() if key.endswith(".metadata")]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from .file_manager_base import FileManagerBase
|
|
5
|
+
from ..file_service_constants import FS_PROTOCOL
|
|
6
|
+
|
|
7
|
+
DEFAULT_DIRECTORY = f"/tmp/{FS_PROTOCOL}"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VolumeFileManager(FileManagerBase):
|
|
11
|
+
|
|
12
|
+
def __init__(self, config, ttl):
|
|
13
|
+
self.config = config
|
|
14
|
+
self.ttl = ttl
|
|
15
|
+
self.shared_volume_directory = config.get("directory", DEFAULT_DIRECTORY)
|
|
16
|
+
if not os.path.exists(self.shared_volume_directory):
|
|
17
|
+
os.makedirs(self.shared_volume_directory)
|
|
18
|
+
|
|
19
|
+
def _save_metadata(self, file_path: str, metadata: dict):
|
|
20
|
+
metadata_path = self._get_metadata_name(file_path)
|
|
21
|
+
with open(metadata_path, "w", encoding="utf-8") as metadata_file:
|
|
22
|
+
json.dump(metadata, metadata_file, indent=4)
|
|
23
|
+
|
|
24
|
+
def upload_from_buffer(self, buffer: bytes, file_name: str, **kwargs) -> dict:
|
|
25
|
+
file_signature = self._generate_file_signature(file_name)
|
|
26
|
+
file_path = os.path.join(self.shared_volume_directory, file_signature)
|
|
27
|
+
|
|
28
|
+
metadata = self._create_metadata(file_signature, file_name, buffer, kwargs)
|
|
29
|
+
|
|
30
|
+
with open(file_path, "wb") as file:
|
|
31
|
+
file.write(buffer)
|
|
32
|
+
|
|
33
|
+
self._save_metadata(file_path, metadata)
|
|
34
|
+
|
|
35
|
+
return metadata
|
|
36
|
+
|
|
37
|
+
def upload_from_file(self, file_path: str, **kwargs) -> dict:
|
|
38
|
+
if not os.path.isfile(file_path):
|
|
39
|
+
raise FileNotFoundError(f"The file {file_path} does not exist.")
|
|
40
|
+
|
|
41
|
+
file_name = os.path.basename(file_path)
|
|
42
|
+
|
|
43
|
+
with open(file_path, "rb") as source_file:
|
|
44
|
+
buffer = source_file.read()
|
|
45
|
+
|
|
46
|
+
return self.upload_from_buffer(buffer, file_name, **kwargs)
|
|
47
|
+
|
|
48
|
+
def download_to_buffer(self, file_name: str) -> bytes:
|
|
49
|
+
file_path = os.path.join(self.shared_volume_directory, file_name)
|
|
50
|
+
if not os.path.exists(file_path):
|
|
51
|
+
raise FileNotFoundError(
|
|
52
|
+
f"The file at {file_name} does not exist in the shared volume."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
with open(file_path, "rb") as file:
|
|
56
|
+
buffer = file.read()
|
|
57
|
+
|
|
58
|
+
return buffer
|
|
59
|
+
|
|
60
|
+
def download_to_file(self, file_name: str, destination_path: str):
|
|
61
|
+
buffer = self.download_to_buffer(file_name)
|
|
62
|
+
with open(destination_path, "wb") as destination_file:
|
|
63
|
+
destination_file.write(buffer)
|
|
64
|
+
|
|
65
|
+
def get_metadata(self, file_name: str) -> dict:
|
|
66
|
+
metadata_name = self._get_metadata_name(file_name)
|
|
67
|
+
metadata_path = os.path.join(self.shared_volume_directory, metadata_name)
|
|
68
|
+
|
|
69
|
+
if not os.path.exists(metadata_path):
|
|
70
|
+
raise FileNotFoundError(
|
|
71
|
+
f"The metadata for the file at {file_name} does not exist."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
with open(metadata_path, "r", encoding="utf-8") as metadata_file:
|
|
75
|
+
metadata = json.load(metadata_file)
|
|
76
|
+
|
|
77
|
+
return metadata
|
|
78
|
+
|
|
79
|
+
def delete_by_name(self, file_name: str):
|
|
80
|
+
file_path = os.path.join(self.shared_volume_directory, file_name)
|
|
81
|
+
metadata_path = self._get_metadata_name(file_path)
|
|
82
|
+
|
|
83
|
+
# Delete the main file
|
|
84
|
+
if os.path.exists(file_path):
|
|
85
|
+
os.remove(file_path)
|
|
86
|
+
os.remove(metadata_path)
|
|
87
|
+
else:
|
|
88
|
+
raise FileNotFoundError(
|
|
89
|
+
f"The file at {file_name} does not exist in the shared volume."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def update_file_expiration(self, file_signature, expiration_timestamp):
|
|
93
|
+
metadata = self.get_metadata(file_signature)
|
|
94
|
+
metadata["expiration_timestamp"] = expiration_timestamp
|
|
95
|
+
file_path = os.path.join(self.shared_volume_directory, file_signature)
|
|
96
|
+
self._save_metadata(file_path, metadata)
|
|
97
|
+
|
|
98
|
+
def list_all_metadata(self) -> list:
|
|
99
|
+
all_metadata = []
|
|
100
|
+
for file_name in os.listdir(self.shared_volume_directory):
|
|
101
|
+
if file_name.endswith(".metadata"):
|
|
102
|
+
metadata_path = os.path.join(self.shared_volume_directory, file_name)
|
|
103
|
+
with open(metadata_path, "r", encoding="utf-8") as metadata_file:
|
|
104
|
+
metadata = json.load(metadata_file)
|
|
105
|
+
all_metadata.append(metadata)
|
|
106
|
+
return all_metadata
|