digitalkin 0.3.2.dev2__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.
- base_server/__init__.py +1 -0
- base_server/mock/__init__.py +5 -0
- base_server/mock/mock_pb2.py +39 -0
- base_server/mock/mock_pb2_grpc.py +102 -0
- base_server/server_async_insecure.py +125 -0
- base_server/server_async_secure.py +143 -0
- base_server/server_sync_insecure.py +103 -0
- base_server/server_sync_secure.py +122 -0
- digitalkin/__init__.py +8 -0
- digitalkin/__version__.py +8 -0
- digitalkin/core/__init__.py +1 -0
- digitalkin/core/common/__init__.py +9 -0
- digitalkin/core/common/factories.py +156 -0
- digitalkin/core/job_manager/__init__.py +1 -0
- digitalkin/core/job_manager/base_job_manager.py +288 -0
- digitalkin/core/job_manager/single_job_manager.py +354 -0
- digitalkin/core/job_manager/taskiq_broker.py +311 -0
- digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
- digitalkin/core/task_manager/__init__.py +1 -0
- digitalkin/core/task_manager/base_task_manager.py +539 -0
- digitalkin/core/task_manager/local_task_manager.py +108 -0
- digitalkin/core/task_manager/remote_task_manager.py +87 -0
- digitalkin/core/task_manager/surrealdb_repository.py +266 -0
- digitalkin/core/task_manager/task_executor.py +249 -0
- digitalkin/core/task_manager/task_session.py +406 -0
- digitalkin/grpc_servers/__init__.py +1 -0
- digitalkin/grpc_servers/_base_server.py +486 -0
- digitalkin/grpc_servers/module_server.py +208 -0
- digitalkin/grpc_servers/module_servicer.py +516 -0
- digitalkin/grpc_servers/utils/__init__.py +1 -0
- digitalkin/grpc_servers/utils/exceptions.py +29 -0
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +88 -0
- digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
- digitalkin/grpc_servers/utils/utility_schema_extender.py +97 -0
- digitalkin/logger.py +157 -0
- digitalkin/mixins/__init__.py +19 -0
- digitalkin/mixins/base_mixin.py +10 -0
- digitalkin/mixins/callback_mixin.py +24 -0
- digitalkin/mixins/chat_history_mixin.py +110 -0
- digitalkin/mixins/cost_mixin.py +76 -0
- digitalkin/mixins/file_history_mixin.py +93 -0
- digitalkin/mixins/filesystem_mixin.py +46 -0
- digitalkin/mixins/logger_mixin.py +51 -0
- digitalkin/mixins/storage_mixin.py +79 -0
- digitalkin/models/__init__.py +8 -0
- digitalkin/models/core/__init__.py +1 -0
- digitalkin/models/core/job_manager_models.py +36 -0
- digitalkin/models/core/task_monitor.py +70 -0
- digitalkin/models/grpc_servers/__init__.py +1 -0
- digitalkin/models/grpc_servers/models.py +275 -0
- digitalkin/models/grpc_servers/types.py +24 -0
- digitalkin/models/module/__init__.py +25 -0
- digitalkin/models/module/module.py +40 -0
- digitalkin/models/module/module_context.py +149 -0
- digitalkin/models/module/module_types.py +393 -0
- digitalkin/models/module/utility.py +146 -0
- digitalkin/models/services/__init__.py +10 -0
- digitalkin/models/services/cost.py +54 -0
- digitalkin/models/services/registry.py +42 -0
- digitalkin/models/services/storage.py +44 -0
- digitalkin/modules/__init__.py +11 -0
- digitalkin/modules/_base_module.py +517 -0
- digitalkin/modules/archetype_module.py +23 -0
- digitalkin/modules/tool_module.py +23 -0
- digitalkin/modules/trigger_handler.py +48 -0
- digitalkin/modules/triggers/__init__.py +12 -0
- digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
- digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
- digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
- digitalkin/py.typed +0 -0
- digitalkin/services/__init__.py +30 -0
- digitalkin/services/agent/__init__.py +6 -0
- digitalkin/services/agent/agent_strategy.py +19 -0
- digitalkin/services/agent/default_agent.py +13 -0
- digitalkin/services/base_strategy.py +22 -0
- digitalkin/services/communication/__init__.py +7 -0
- digitalkin/services/communication/communication_strategy.py +76 -0
- digitalkin/services/communication/default_communication.py +101 -0
- digitalkin/services/communication/grpc_communication.py +223 -0
- digitalkin/services/cost/__init__.py +14 -0
- digitalkin/services/cost/cost_strategy.py +100 -0
- digitalkin/services/cost/default_cost.py +114 -0
- digitalkin/services/cost/grpc_cost.py +138 -0
- digitalkin/services/filesystem/__init__.py +7 -0
- digitalkin/services/filesystem/default_filesystem.py +417 -0
- digitalkin/services/filesystem/filesystem_strategy.py +252 -0
- digitalkin/services/filesystem/grpc_filesystem.py +317 -0
- digitalkin/services/identity/__init__.py +6 -0
- digitalkin/services/identity/default_identity.py +15 -0
- digitalkin/services/identity/identity_strategy.py +14 -0
- digitalkin/services/registry/__init__.py +27 -0
- digitalkin/services/registry/default_registry.py +141 -0
- digitalkin/services/registry/exceptions.py +47 -0
- digitalkin/services/registry/grpc_registry.py +306 -0
- digitalkin/services/registry/registry_models.py +43 -0
- digitalkin/services/registry/registry_strategy.py +98 -0
- digitalkin/services/services_config.py +200 -0
- digitalkin/services/services_models.py +65 -0
- digitalkin/services/setup/__init__.py +1 -0
- digitalkin/services/setup/default_setup.py +219 -0
- digitalkin/services/setup/grpc_setup.py +343 -0
- digitalkin/services/setup/setup_strategy.py +145 -0
- digitalkin/services/snapshot/__init__.py +6 -0
- digitalkin/services/snapshot/default_snapshot.py +39 -0
- digitalkin/services/snapshot/snapshot_strategy.py +30 -0
- digitalkin/services/storage/__init__.py +7 -0
- digitalkin/services/storage/default_storage.py +228 -0
- digitalkin/services/storage/grpc_storage.py +214 -0
- digitalkin/services/storage/storage_strategy.py +273 -0
- digitalkin/services/user_profile/__init__.py +12 -0
- digitalkin/services/user_profile/default_user_profile.py +55 -0
- digitalkin/services/user_profile/grpc_user_profile.py +69 -0
- digitalkin/services/user_profile/user_profile_strategy.py +40 -0
- digitalkin/utils/__init__.py +29 -0
- digitalkin/utils/arg_parser.py +92 -0
- digitalkin/utils/development_mode_action.py +51 -0
- digitalkin/utils/dynamic_schema.py +483 -0
- digitalkin/utils/llm_ready_schema.py +75 -0
- digitalkin/utils/package_discover.py +357 -0
- digitalkin-0.3.2.dev2.dist-info/METADATA +602 -0
- digitalkin-0.3.2.dev2.dist-info/RECORD +131 -0
- digitalkin-0.3.2.dev2.dist-info/WHEEL +5 -0
- digitalkin-0.3.2.dev2.dist-info/licenses/LICENSE +430 -0
- digitalkin-0.3.2.dev2.dist-info/top_level.txt +4 -0
- modules/__init__.py +0 -0
- modules/cpu_intensive_module.py +280 -0
- modules/dynamic_setup_module.py +338 -0
- modules/minimal_llm_module.py +347 -0
- modules/text_transform_module.py +203 -0
- services/filesystem_module.py +200 -0
- services/storage_module.py +206 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Default filesystem implementation."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from digitalkin.logger import logger
|
|
11
|
+
from digitalkin.services.filesystem.filesystem_strategy import (
|
|
12
|
+
FileFilter,
|
|
13
|
+
FilesystemRecord,
|
|
14
|
+
FilesystemServiceError,
|
|
15
|
+
FilesystemStrategy,
|
|
16
|
+
UploadFileData,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DefaultFilesystem(FilesystemStrategy):
|
|
21
|
+
"""Default filesystem implementation.
|
|
22
|
+
|
|
23
|
+
This implementation provides a local filesystem-based storage solution
|
|
24
|
+
with support for all filesystem operations defined in the strategy.
|
|
25
|
+
Files are stored in a temporary directory with proper metadata tracking.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, mission_id: str, setup_id: str, setup_version_id: str) -> None:
|
|
29
|
+
"""Initialize the default filesystem strategy.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
mission_id: The ID of the mission this strategy is associated with
|
|
33
|
+
setup_id: The ID of the setup
|
|
34
|
+
setup_version_id: The ID of the setup version this strategy is associated with
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(mission_id, setup_id, setup_version_id)
|
|
37
|
+
self.temp_root: str = tempfile.mkdtemp()
|
|
38
|
+
os.makedirs(self.temp_root, exist_ok=True)
|
|
39
|
+
self.db: dict[str, FilesystemRecord] = {}
|
|
40
|
+
logger.debug("DefaultFilesystem initialized with temp_root: %s", self.temp_root)
|
|
41
|
+
|
|
42
|
+
def _get_context_temp_dir(self, context: str) -> str:
|
|
43
|
+
"""Get the temporary directory path for a specific context.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
context: The mission ID or setup ID.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: Path to the context's temporary directory
|
|
50
|
+
"""
|
|
51
|
+
# Create a context-specific directory to organize files
|
|
52
|
+
context_dir = os.path.join(self.temp_root, context.replace(":", "_"))
|
|
53
|
+
os.makedirs(context_dir, exist_ok=True)
|
|
54
|
+
return context_dir
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _calculate_checksum(content: bytes) -> str:
|
|
58
|
+
"""Calculate SHA-256 checksum of content.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
content: The content to calculate checksum for
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
str: The SHA-256 checksum
|
|
65
|
+
"""
|
|
66
|
+
return hashlib.sha256(content).hexdigest()
|
|
67
|
+
|
|
68
|
+
def _filter_db(
|
|
69
|
+
self,
|
|
70
|
+
filters: FileFilter,
|
|
71
|
+
) -> list[FilesystemRecord]:
|
|
72
|
+
"""Filter the in-memory database based on provided filters.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
filters: Filter criteria for the files
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
list[FilesystemRecord]: List of files matching the filters
|
|
79
|
+
"""
|
|
80
|
+
logger.debug("Filtering db with filters: %s", filters)
|
|
81
|
+
return [
|
|
82
|
+
f
|
|
83
|
+
for f in self.db.values()
|
|
84
|
+
if (not filters.names or f.name in filters.names)
|
|
85
|
+
and (not filters.file_ids or f.id in filters.file_ids)
|
|
86
|
+
and (not filters.file_types or f.file_type in filters.file_types)
|
|
87
|
+
and (not filters.status or f.status == filters.status)
|
|
88
|
+
and (not filters.content_type_prefix or f.content_type.startswith(filters.content_type_prefix))
|
|
89
|
+
and (not filters.min_size_bytes or f.size_bytes >= filters.min_size_bytes)
|
|
90
|
+
and (not filters.max_size_bytes or f.size_bytes <= filters.max_size_bytes)
|
|
91
|
+
and (not filters.prefix or f.name.startswith(filters.prefix))
|
|
92
|
+
and (not filters.content_type or f.content_type == filters.content_type)
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
def upload_files(
|
|
96
|
+
self,
|
|
97
|
+
files: list[UploadFileData],
|
|
98
|
+
) -> tuple[list[FilesystemRecord], int, int]:
|
|
99
|
+
"""Upload multiple files to the system.
|
|
100
|
+
|
|
101
|
+
This method allows batch uploading of files with validation and
|
|
102
|
+
error handling for each individual file. Files are processed
|
|
103
|
+
atomically - if one fails, others may still succeed.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
files: List of files to upload
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
tuple[list[FilesystemRecord], int, int]: List of uploaded files, total uploaded count, total failed count
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
FilesystemServiceError: If there is an error uploading the files
|
|
113
|
+
"""
|
|
114
|
+
uploaded_files: list[FilesystemRecord] = []
|
|
115
|
+
total_uploaded = 0
|
|
116
|
+
total_failed = 0
|
|
117
|
+
|
|
118
|
+
for file in files:
|
|
119
|
+
try:
|
|
120
|
+
# Check if file with same name exists in the context
|
|
121
|
+
context_dir = self._get_context_temp_dir(self.setup_id)
|
|
122
|
+
file_path = os.path.join(context_dir, file.name)
|
|
123
|
+
if os.path.exists(file_path) and not file.replace_if_exists:
|
|
124
|
+
msg = f"File with name {file.name} already exists."
|
|
125
|
+
logger.error(msg)
|
|
126
|
+
raise FilesystemServiceError(msg) # noqa: TRY301
|
|
127
|
+
|
|
128
|
+
Path(file_path).write_bytes(file.content)
|
|
129
|
+
storage_uri = str(Path(file_path).resolve())
|
|
130
|
+
file_data = FilesystemRecord(
|
|
131
|
+
id=str(uuid.uuid4()),
|
|
132
|
+
context=self.setup_id,
|
|
133
|
+
name=file.name,
|
|
134
|
+
file_type=file.file_type,
|
|
135
|
+
content_type=file.content_type or "application/octet-stream",
|
|
136
|
+
size_bytes=len(file.content),
|
|
137
|
+
checksum=self._calculate_checksum(file.content),
|
|
138
|
+
metadata=file.metadata,
|
|
139
|
+
storage_uri=storage_uri,
|
|
140
|
+
file_url=storage_uri,
|
|
141
|
+
status="ACTIVE",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
self.db[file_data.id] = file_data
|
|
145
|
+
uploaded_files.append(file_data)
|
|
146
|
+
total_uploaded += 1
|
|
147
|
+
logger.debug("Uploaded file %s", file_data)
|
|
148
|
+
except Exception as e: # noqa: PERF203
|
|
149
|
+
logger.exception("Error uploading file %s: %s", file.name, e)
|
|
150
|
+
total_failed += 1
|
|
151
|
+
# If only one file and it failed, propagate the error for pytest.raises
|
|
152
|
+
if len(files) == 1:
|
|
153
|
+
raise
|
|
154
|
+
|
|
155
|
+
return uploaded_files, total_uploaded, total_failed
|
|
156
|
+
|
|
157
|
+
def get_files(
|
|
158
|
+
self,
|
|
159
|
+
filters: FileFilter,
|
|
160
|
+
*,
|
|
161
|
+
list_size: int = 100,
|
|
162
|
+
offset: int = 0,
|
|
163
|
+
order: str | None = None, # noqa: ARG002
|
|
164
|
+
include_content: bool = False,
|
|
165
|
+
) -> tuple[list[FilesystemRecord], int]:
|
|
166
|
+
"""List files with filtering, sorting, and pagination.
|
|
167
|
+
|
|
168
|
+
This method provides flexible file querying capabilities with support for:
|
|
169
|
+
- Multiple filter criteria (name, type, dates, size, etc.)
|
|
170
|
+
- Pagination for large result sets
|
|
171
|
+
- Sorting by various fields
|
|
172
|
+
- Scoped access by context
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
filters: Filter criteria for the files
|
|
176
|
+
list_size: Number of files to return per page
|
|
177
|
+
offset: Offset to start listing files from
|
|
178
|
+
order: Fields to order results by (example: "created_at:asc,name:desc")
|
|
179
|
+
include_content: Whether to include file content in response
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
tuple[list[FilesystemRecord], int]: List of files, total count
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
FilesystemServiceError: If there is an error listing the files
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
logger.debug("Listing files with filters: %s", filters)
|
|
189
|
+
# Filter files based on provided criteria
|
|
190
|
+
filtered_files = self._filter_db(filters)
|
|
191
|
+
if not filtered_files:
|
|
192
|
+
return [], 0
|
|
193
|
+
# Sort if order is specified
|
|
194
|
+
# TODO
|
|
195
|
+
|
|
196
|
+
# Apply pagination
|
|
197
|
+
start_idx = offset
|
|
198
|
+
end_idx = start_idx + list_size
|
|
199
|
+
paginated_files = filtered_files[start_idx:end_idx]
|
|
200
|
+
|
|
201
|
+
if include_content:
|
|
202
|
+
for file in paginated_files:
|
|
203
|
+
file.content = Path(file.storage_uri).read_bytes()
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
msg = f"Error listing files: {e!s}"
|
|
207
|
+
logger.exception(msg)
|
|
208
|
+
raise FilesystemServiceError(msg)
|
|
209
|
+
else:
|
|
210
|
+
return paginated_files, len(filtered_files)
|
|
211
|
+
|
|
212
|
+
def get_file(
|
|
213
|
+
self,
|
|
214
|
+
file_id: str,
|
|
215
|
+
context: Literal["mission", "setup"] = "mission", # noqa: ARG002
|
|
216
|
+
*,
|
|
217
|
+
include_content: bool = False,
|
|
218
|
+
) -> FilesystemRecord:
|
|
219
|
+
"""Get a specific file by ID or name.
|
|
220
|
+
|
|
221
|
+
This method fetches detailed information about a single file,
|
|
222
|
+
with optional content inclusion. Supports lookup by either
|
|
223
|
+
unique ID or name within a context.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
file_id: The ID of the file to be retrieved
|
|
227
|
+
context: The context of the files (mission or setup)
|
|
228
|
+
include_content: Whether to include file content in response
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
FilesystemRecord: Metadata about the retrieved file
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
FilesystemServiceError: If there is an error retrieving the file
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
logger.debug("Getting file with id: %s", file_id)
|
|
238
|
+
file_data: FilesystemRecord | None = None
|
|
239
|
+
if file_id:
|
|
240
|
+
file_data = self.db.get(file_id)
|
|
241
|
+
|
|
242
|
+
if not file_data:
|
|
243
|
+
msg = f"File not found with id {file_id}"
|
|
244
|
+
logger.error(msg)
|
|
245
|
+
raise FilesystemServiceError(msg) # noqa: TRY301
|
|
246
|
+
|
|
247
|
+
if include_content:
|
|
248
|
+
file_path = file_data.storage_uri
|
|
249
|
+
if os.path.exists(file_path):
|
|
250
|
+
content = Path(file_path).read_bytes()
|
|
251
|
+
file_data.content = content
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
msg = f"Error getting file: {e!s}"
|
|
255
|
+
logger.exception(msg)
|
|
256
|
+
raise FilesystemServiceError(msg)
|
|
257
|
+
else:
|
|
258
|
+
return file_data
|
|
259
|
+
|
|
260
|
+
def update_file(
|
|
261
|
+
self,
|
|
262
|
+
file_id: str,
|
|
263
|
+
content: bytes | None = None,
|
|
264
|
+
file_type: Literal[
|
|
265
|
+
"UNSPECIFIED",
|
|
266
|
+
"DOCUMENT",
|
|
267
|
+
"IMAGE",
|
|
268
|
+
"VIDEO",
|
|
269
|
+
"AUDIO",
|
|
270
|
+
"ARCHIVE",
|
|
271
|
+
"CODE",
|
|
272
|
+
"OTHER",
|
|
273
|
+
]
|
|
274
|
+
| None = None,
|
|
275
|
+
content_type: str | None = None,
|
|
276
|
+
metadata: dict[str, Any] | None = None,
|
|
277
|
+
new_name: str | None = None,
|
|
278
|
+
status: str | None = None,
|
|
279
|
+
) -> FilesystemRecord:
|
|
280
|
+
"""Update file metadata, content, or both.
|
|
281
|
+
|
|
282
|
+
This method allows updating various aspects of a file:
|
|
283
|
+
- Rename files
|
|
284
|
+
- Update content and content type
|
|
285
|
+
- Modify metadata
|
|
286
|
+
- Create new versions
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
file_id: The id of the file to be updated
|
|
290
|
+
content: Optional new content of the file
|
|
291
|
+
file_type: Optional new type of data
|
|
292
|
+
content_type: Optional new MIME type
|
|
293
|
+
metadata: Optional new metadata (will merge with existing)
|
|
294
|
+
new_name: Optional new name for the file
|
|
295
|
+
status: Optional new status for the file
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
FilesystemRecord: Metadata about the updated file
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
FilesystemServiceError: If there is an error during update
|
|
302
|
+
"""
|
|
303
|
+
logger.debug("Updating file with id: %s", file_id)
|
|
304
|
+
if file_id not in self.db:
|
|
305
|
+
msg = f"File with id {file_id} does not exist."
|
|
306
|
+
logger.error(msg)
|
|
307
|
+
raise FilesystemServiceError(msg)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
context_dir = self._get_context_temp_dir(self.setup_id)
|
|
311
|
+
file_path = os.path.join(context_dir, file_id)
|
|
312
|
+
existing_file = self.db[file_id]
|
|
313
|
+
|
|
314
|
+
if content is not None:
|
|
315
|
+
Path(file_path).write_bytes(content)
|
|
316
|
+
existing_file.size_bytes = len(content)
|
|
317
|
+
existing_file.checksum = self._calculate_checksum(content)
|
|
318
|
+
|
|
319
|
+
if file_type is not None:
|
|
320
|
+
existing_file.file_type = file_type
|
|
321
|
+
|
|
322
|
+
if content_type is not None:
|
|
323
|
+
existing_file.content_type = content_type
|
|
324
|
+
|
|
325
|
+
if metadata is not None:
|
|
326
|
+
existing_file.metadata = metadata
|
|
327
|
+
|
|
328
|
+
if status is not None:
|
|
329
|
+
existing_file.status = status
|
|
330
|
+
|
|
331
|
+
if new_name is not None:
|
|
332
|
+
new_path = os.path.join(context_dir, new_name)
|
|
333
|
+
os.rename(file_path, new_path)
|
|
334
|
+
existing_file.name = new_name
|
|
335
|
+
existing_file.storage_uri = str(Path(new_path).resolve())
|
|
336
|
+
|
|
337
|
+
self.db[file_id] = existing_file
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
msg = f"Error updating file {file_id}: {e!s}"
|
|
341
|
+
logger.exception(msg)
|
|
342
|
+
raise FilesystemServiceError(msg)
|
|
343
|
+
else:
|
|
344
|
+
return existing_file
|
|
345
|
+
|
|
346
|
+
def delete_files(
|
|
347
|
+
self,
|
|
348
|
+
filters: FileFilter,
|
|
349
|
+
*,
|
|
350
|
+
permanent: bool = False,
|
|
351
|
+
force: bool = False, # noqa: ARG002
|
|
352
|
+
) -> tuple[dict[str, bool], int, int]:
|
|
353
|
+
"""Delete multiple files.
|
|
354
|
+
|
|
355
|
+
This method supports batch deletion of files with options for:
|
|
356
|
+
- Soft deletion (marking as deleted)
|
|
357
|
+
- Permanent deletion
|
|
358
|
+
- Force deletion of files in use
|
|
359
|
+
- Individual error reporting per file
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
filters: Filter criteria for the files to delete
|
|
363
|
+
permanent: Whether to permanently delete the files
|
|
364
|
+
force: Whether to force delete even if files are in use
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
tuple[dict[str, bool], int, int]: Results per file, total deleted count, total failed count
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
FilesystemServiceError: If there is an error deleting the files
|
|
371
|
+
"""
|
|
372
|
+
logger.debug("Deleting files with filters: %s", filters)
|
|
373
|
+
results: dict[str, bool] = {} # id -> success
|
|
374
|
+
total_deleted = 0
|
|
375
|
+
total_failed = 0
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
# Determine which files to delete
|
|
379
|
+
files_to_delete = [f.id for f in self._filter_db(filters)]
|
|
380
|
+
|
|
381
|
+
if not files_to_delete:
|
|
382
|
+
logger.info("No files match the deletion criteria.")
|
|
383
|
+
return results, total_deleted, total_failed
|
|
384
|
+
|
|
385
|
+
for file_id in files_to_delete:
|
|
386
|
+
file_data = self.db[file_id]
|
|
387
|
+
if not file_data:
|
|
388
|
+
results[file_id] = False
|
|
389
|
+
total_failed += 1
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
file_path = file_data.storage_uri
|
|
394
|
+
if os.path.exists(file_path):
|
|
395
|
+
if permanent:
|
|
396
|
+
os.remove(file_path)
|
|
397
|
+
del self.db[file_id]
|
|
398
|
+
else:
|
|
399
|
+
file_data.status = "DELETED"
|
|
400
|
+
self.db[file_id] = file_data
|
|
401
|
+
results[file_id] = True
|
|
402
|
+
total_deleted += 1
|
|
403
|
+
else:
|
|
404
|
+
results[file_id] = False
|
|
405
|
+
total_failed += 1
|
|
406
|
+
except Exception as e:
|
|
407
|
+
logger.exception("Error deleting file %s: %s", file_id, e)
|
|
408
|
+
results[file_id] = False
|
|
409
|
+
total_failed += 1
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
msg = f"Error in delete_files: {e!s}"
|
|
413
|
+
logger.exception(msg)
|
|
414
|
+
raise FilesystemServiceError(msg)
|
|
415
|
+
|
|
416
|
+
else:
|
|
417
|
+
return results, total_deleted, total_failed
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""This module contains the abstract base class for filesystem strategies."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from digitalkin.services.base_strategy import BaseStrategy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FilesystemServiceError(Exception):
|
|
13
|
+
"""Base exception for Filesystem service errors."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FilesystemRecord(BaseModel):
|
|
17
|
+
"""Data model for filesystem operations."""
|
|
18
|
+
|
|
19
|
+
id: str = Field(description="Unique identifier for the file (UUID)")
|
|
20
|
+
context: str = Field(description="The context of the file in the filesystem")
|
|
21
|
+
name: str = Field(description="The name of the file")
|
|
22
|
+
file_type: str = Field(default="UNSPECIFIED", description="The type of data stored")
|
|
23
|
+
content_type: str = Field(default="application/octet-stream", description="The MIME type of the file")
|
|
24
|
+
size_bytes: int = Field(default=0, description="Size of the file in bytes")
|
|
25
|
+
checksum: str = Field(default="", description="SHA-256 checksum of the file content")
|
|
26
|
+
metadata: dict[str, Any] | None = Field(default=None, description="Additional metadata for the file")
|
|
27
|
+
storage_uri: str = Field(description="Internal URI for accessing the file content")
|
|
28
|
+
file_url: str = Field(description="Public URL for accessing the file content")
|
|
29
|
+
status: str = Field(default="UNSPECIFIED", description="Current status of the file")
|
|
30
|
+
content: bytes | None = Field(default=None, description="The content of the file")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FileFilter(BaseModel):
|
|
34
|
+
"""Filter criteria for querying files."""
|
|
35
|
+
|
|
36
|
+
context: Literal["mission", "setup"] = Field(
|
|
37
|
+
default="mission", description="The context of the files (mission or setup)"
|
|
38
|
+
)
|
|
39
|
+
names: list[str] | None = Field(default=None, description="Filter by file names (exact matches)")
|
|
40
|
+
file_ids: list[str] | None = Field(default=None, description="Filter by file IDs")
|
|
41
|
+
file_types: (
|
|
42
|
+
list[
|
|
43
|
+
Literal[
|
|
44
|
+
"UNSPECIFIED",
|
|
45
|
+
"DOCUMENT",
|
|
46
|
+
"IMAGE",
|
|
47
|
+
"AUDIO",
|
|
48
|
+
"VIDEO",
|
|
49
|
+
"ARCHIVE",
|
|
50
|
+
"CODE",
|
|
51
|
+
"OTHER",
|
|
52
|
+
]
|
|
53
|
+
]
|
|
54
|
+
| None
|
|
55
|
+
) = Field(default=None, description="Filter by file types")
|
|
56
|
+
created_after: datetime | None = Field(default=None, description="Filter files created after this timestamp")
|
|
57
|
+
created_before: datetime | None = Field(default=None, description="Filter files created before this timestamp")
|
|
58
|
+
updated_after: datetime | None = Field(default=None, description="Filter files updated after this timestamp")
|
|
59
|
+
updated_before: datetime | None = Field(default=None, description="Filter files updated before this timestamp")
|
|
60
|
+
status: str | None = Field(default=None, description="Filter by file status")
|
|
61
|
+
content_type_prefix: str | None = Field(default=None, description="Filter by content type prefix (e.g., 'image/')")
|
|
62
|
+
min_size_bytes: int | None = Field(default=None, description="Filter files with minimum size")
|
|
63
|
+
max_size_bytes: int | None = Field(default=None, description="Filter files with maximum size")
|
|
64
|
+
prefix: str | None = Field(default=None, description="Filter by path prefix (e.g., 'folder1/')")
|
|
65
|
+
content_type: str | None = Field(default=None, description="Filter by content type")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class UploadFileData(BaseModel):
|
|
69
|
+
"""Data model for uploading a file."""
|
|
70
|
+
|
|
71
|
+
content: bytes = Field(description="The content of the file")
|
|
72
|
+
name: str = Field(description="The name of the file")
|
|
73
|
+
file_type: Literal[
|
|
74
|
+
"UNSPECIFIED",
|
|
75
|
+
"DOCUMENT",
|
|
76
|
+
"IMAGE",
|
|
77
|
+
"AUDIO",
|
|
78
|
+
"VIDEO",
|
|
79
|
+
"ARCHIVE",
|
|
80
|
+
"CODE",
|
|
81
|
+
"OTHER",
|
|
82
|
+
] = Field(description="The type of the file")
|
|
83
|
+
content_type: str | None = Field(default=None, description="The content type of the file")
|
|
84
|
+
metadata: dict[str, Any] | None = Field(default=None, description="The metadata of the file")
|
|
85
|
+
replace_if_exists: bool = Field(default=False, description="Whether to replace the file if it already exists")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FilesystemStrategy(BaseStrategy, ABC):
|
|
89
|
+
"""Abstract base class for filesystem strategies.
|
|
90
|
+
|
|
91
|
+
This strategy provides comprehensive file management capabilities including
|
|
92
|
+
upload, retrieval, update, and deletion operations with rich metadata support,
|
|
93
|
+
filtering, and pagination.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
mission_id: str,
|
|
99
|
+
setup_id: str,
|
|
100
|
+
setup_version_id: str,
|
|
101
|
+
config: dict[str, Any] | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Initialize the strategy.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
mission_id: The ID of the mission this strategy is associated with
|
|
107
|
+
setup_id: The ID of the setup
|
|
108
|
+
setup_version_id: The ID of the setup version this strategy is associated with
|
|
109
|
+
config: Configuration for the filesystem strategy
|
|
110
|
+
"""
|
|
111
|
+
super().__init__(mission_id, setup_id, setup_version_id)
|
|
112
|
+
self.config = config
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def upload_files(
|
|
116
|
+
self,
|
|
117
|
+
files: list[UploadFileData],
|
|
118
|
+
) -> tuple[list[FilesystemRecord], int, int]:
|
|
119
|
+
"""Upload multiple files to the system.
|
|
120
|
+
|
|
121
|
+
This method allows batch uploading of files with validation and
|
|
122
|
+
error handling for each individual file. Files are processed
|
|
123
|
+
atomically - if one fails, others may still succeed.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
files: List of tuples containing (content, name, file_type, content_type, metadata, replace_if_exists)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
tuple[list[FilesystemRecord], int, int]: List of uploaded files, total uploaded count, total failed count
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def get_file(
|
|
134
|
+
self,
|
|
135
|
+
file_id: str,
|
|
136
|
+
context: Literal["mission", "setup"] = "mission",
|
|
137
|
+
*,
|
|
138
|
+
include_content: bool = False,
|
|
139
|
+
) -> FilesystemRecord:
|
|
140
|
+
"""Get a specific file by ID or name.
|
|
141
|
+
|
|
142
|
+
This method fetches detailed information about a single file,
|
|
143
|
+
with optional content inclusion. Supports lookup by either
|
|
144
|
+
unique ID or name within a context.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
file_id: The ID of the file to be retrieved
|
|
148
|
+
context: The context of the files (mission or setup)
|
|
149
|
+
include_content: Whether to include file content in response
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
tuple[FilesystemRecord, bytes | None]: Metadata about the retrieved file and optional content
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def get_files(
|
|
157
|
+
self,
|
|
158
|
+
filters: FileFilter,
|
|
159
|
+
*,
|
|
160
|
+
list_size: int = 100,
|
|
161
|
+
offset: int = 0,
|
|
162
|
+
order: str | None = None,
|
|
163
|
+
include_content: bool = False,
|
|
164
|
+
) -> tuple[list[FilesystemRecord], int]:
|
|
165
|
+
"""Get multiple files by various criteria.
|
|
166
|
+
|
|
167
|
+
This method provides efficient retrieval of multiple files using:
|
|
168
|
+
- File IDs
|
|
169
|
+
- File names
|
|
170
|
+
- Path prefix
|
|
171
|
+
With support for:
|
|
172
|
+
- Pagination for large result sets
|
|
173
|
+
- Optional content inclusion
|
|
174
|
+
- Total count of matching files
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
filters: Filter criteria for the files
|
|
178
|
+
list_size: Number of files to return per page
|
|
179
|
+
offset: Offset to start listing files from
|
|
180
|
+
order: Field to order results by
|
|
181
|
+
include_content: Whether to include file content in response
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
tuple[list[FilesystemRecord], int]: List of files and total count
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
@abstractmethod
|
|
188
|
+
def update_file(
|
|
189
|
+
self,
|
|
190
|
+
file_id: str,
|
|
191
|
+
content: bytes | None = None,
|
|
192
|
+
file_type: Literal[
|
|
193
|
+
"UNSPECIFIED",
|
|
194
|
+
"DOCUMENT",
|
|
195
|
+
"IMAGE",
|
|
196
|
+
"VIDEO",
|
|
197
|
+
"AUDIO",
|
|
198
|
+
"ARCHIVE",
|
|
199
|
+
"CODE",
|
|
200
|
+
"OTHER",
|
|
201
|
+
]
|
|
202
|
+
| None = None,
|
|
203
|
+
content_type: str | None = None,
|
|
204
|
+
metadata: dict[str, Any] | None = None,
|
|
205
|
+
new_name: str | None = None,
|
|
206
|
+
status: str | None = None,
|
|
207
|
+
) -> FilesystemRecord:
|
|
208
|
+
"""Update file metadata, content, or both.
|
|
209
|
+
|
|
210
|
+
This method allows updating various aspects of a file:
|
|
211
|
+
- Rename files
|
|
212
|
+
- Update content and content type
|
|
213
|
+
- Modify metadata
|
|
214
|
+
- Create new versions
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
file_id: The ID of the file to be updated
|
|
218
|
+
content: Optional new content of the file
|
|
219
|
+
file_type: Optional new type of data
|
|
220
|
+
content_type: Optional new MIME type
|
|
221
|
+
metadata: Optional new metadata (will merge with existing)
|
|
222
|
+
new_name: Optional new name for the file
|
|
223
|
+
status: Optional new status for the file
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
FilesystemRecord: Metadata about the updated file
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
@abstractmethod
|
|
230
|
+
def delete_files(
|
|
231
|
+
self,
|
|
232
|
+
filters: FileFilter,
|
|
233
|
+
*,
|
|
234
|
+
permanent: bool = False,
|
|
235
|
+
force: bool = False,
|
|
236
|
+
) -> tuple[dict[str, bool], int, int]:
|
|
237
|
+
"""Delete multiple files.
|
|
238
|
+
|
|
239
|
+
This method supports batch deletion of files with options for:
|
|
240
|
+
- Soft deletion (marking as deleted)
|
|
241
|
+
- Permanent deletion
|
|
242
|
+
- Force deletion of files in use
|
|
243
|
+
- Individual error reporting per file
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
filters: Filter criteria for the files
|
|
247
|
+
permanent: Whether to permanently delete the files
|
|
248
|
+
force: Whether to force delete even if files are in use
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
tuple[dict[str, bool], int, int]: Results per file, total deleted count, total failed count
|
|
252
|
+
"""
|