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.
Files changed (131) hide show
  1. base_server/__init__.py +1 -0
  2. base_server/mock/__init__.py +5 -0
  3. base_server/mock/mock_pb2.py +39 -0
  4. base_server/mock/mock_pb2_grpc.py +102 -0
  5. base_server/server_async_insecure.py +125 -0
  6. base_server/server_async_secure.py +143 -0
  7. base_server/server_sync_insecure.py +103 -0
  8. base_server/server_sync_secure.py +122 -0
  9. digitalkin/__init__.py +8 -0
  10. digitalkin/__version__.py +8 -0
  11. digitalkin/core/__init__.py +1 -0
  12. digitalkin/core/common/__init__.py +9 -0
  13. digitalkin/core/common/factories.py +156 -0
  14. digitalkin/core/job_manager/__init__.py +1 -0
  15. digitalkin/core/job_manager/base_job_manager.py +288 -0
  16. digitalkin/core/job_manager/single_job_manager.py +354 -0
  17. digitalkin/core/job_manager/taskiq_broker.py +311 -0
  18. digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
  19. digitalkin/core/task_manager/__init__.py +1 -0
  20. digitalkin/core/task_manager/base_task_manager.py +539 -0
  21. digitalkin/core/task_manager/local_task_manager.py +108 -0
  22. digitalkin/core/task_manager/remote_task_manager.py +87 -0
  23. digitalkin/core/task_manager/surrealdb_repository.py +266 -0
  24. digitalkin/core/task_manager/task_executor.py +249 -0
  25. digitalkin/core/task_manager/task_session.py +406 -0
  26. digitalkin/grpc_servers/__init__.py +1 -0
  27. digitalkin/grpc_servers/_base_server.py +486 -0
  28. digitalkin/grpc_servers/module_server.py +208 -0
  29. digitalkin/grpc_servers/module_servicer.py +516 -0
  30. digitalkin/grpc_servers/utils/__init__.py +1 -0
  31. digitalkin/grpc_servers/utils/exceptions.py +29 -0
  32. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +88 -0
  33. digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
  34. digitalkin/grpc_servers/utils/utility_schema_extender.py +97 -0
  35. digitalkin/logger.py +157 -0
  36. digitalkin/mixins/__init__.py +19 -0
  37. digitalkin/mixins/base_mixin.py +10 -0
  38. digitalkin/mixins/callback_mixin.py +24 -0
  39. digitalkin/mixins/chat_history_mixin.py +110 -0
  40. digitalkin/mixins/cost_mixin.py +76 -0
  41. digitalkin/mixins/file_history_mixin.py +93 -0
  42. digitalkin/mixins/filesystem_mixin.py +46 -0
  43. digitalkin/mixins/logger_mixin.py +51 -0
  44. digitalkin/mixins/storage_mixin.py +79 -0
  45. digitalkin/models/__init__.py +8 -0
  46. digitalkin/models/core/__init__.py +1 -0
  47. digitalkin/models/core/job_manager_models.py +36 -0
  48. digitalkin/models/core/task_monitor.py +70 -0
  49. digitalkin/models/grpc_servers/__init__.py +1 -0
  50. digitalkin/models/grpc_servers/models.py +275 -0
  51. digitalkin/models/grpc_servers/types.py +24 -0
  52. digitalkin/models/module/__init__.py +25 -0
  53. digitalkin/models/module/module.py +40 -0
  54. digitalkin/models/module/module_context.py +149 -0
  55. digitalkin/models/module/module_types.py +393 -0
  56. digitalkin/models/module/utility.py +146 -0
  57. digitalkin/models/services/__init__.py +10 -0
  58. digitalkin/models/services/cost.py +54 -0
  59. digitalkin/models/services/registry.py +42 -0
  60. digitalkin/models/services/storage.py +44 -0
  61. digitalkin/modules/__init__.py +11 -0
  62. digitalkin/modules/_base_module.py +517 -0
  63. digitalkin/modules/archetype_module.py +23 -0
  64. digitalkin/modules/tool_module.py +23 -0
  65. digitalkin/modules/trigger_handler.py +48 -0
  66. digitalkin/modules/triggers/__init__.py +12 -0
  67. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  68. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  69. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  70. digitalkin/py.typed +0 -0
  71. digitalkin/services/__init__.py +30 -0
  72. digitalkin/services/agent/__init__.py +6 -0
  73. digitalkin/services/agent/agent_strategy.py +19 -0
  74. digitalkin/services/agent/default_agent.py +13 -0
  75. digitalkin/services/base_strategy.py +22 -0
  76. digitalkin/services/communication/__init__.py +7 -0
  77. digitalkin/services/communication/communication_strategy.py +76 -0
  78. digitalkin/services/communication/default_communication.py +101 -0
  79. digitalkin/services/communication/grpc_communication.py +223 -0
  80. digitalkin/services/cost/__init__.py +14 -0
  81. digitalkin/services/cost/cost_strategy.py +100 -0
  82. digitalkin/services/cost/default_cost.py +114 -0
  83. digitalkin/services/cost/grpc_cost.py +138 -0
  84. digitalkin/services/filesystem/__init__.py +7 -0
  85. digitalkin/services/filesystem/default_filesystem.py +417 -0
  86. digitalkin/services/filesystem/filesystem_strategy.py +252 -0
  87. digitalkin/services/filesystem/grpc_filesystem.py +317 -0
  88. digitalkin/services/identity/__init__.py +6 -0
  89. digitalkin/services/identity/default_identity.py +15 -0
  90. digitalkin/services/identity/identity_strategy.py +14 -0
  91. digitalkin/services/registry/__init__.py +27 -0
  92. digitalkin/services/registry/default_registry.py +141 -0
  93. digitalkin/services/registry/exceptions.py +47 -0
  94. digitalkin/services/registry/grpc_registry.py +306 -0
  95. digitalkin/services/registry/registry_models.py +43 -0
  96. digitalkin/services/registry/registry_strategy.py +98 -0
  97. digitalkin/services/services_config.py +200 -0
  98. digitalkin/services/services_models.py +65 -0
  99. digitalkin/services/setup/__init__.py +1 -0
  100. digitalkin/services/setup/default_setup.py +219 -0
  101. digitalkin/services/setup/grpc_setup.py +343 -0
  102. digitalkin/services/setup/setup_strategy.py +145 -0
  103. digitalkin/services/snapshot/__init__.py +6 -0
  104. digitalkin/services/snapshot/default_snapshot.py +39 -0
  105. digitalkin/services/snapshot/snapshot_strategy.py +30 -0
  106. digitalkin/services/storage/__init__.py +7 -0
  107. digitalkin/services/storage/default_storage.py +228 -0
  108. digitalkin/services/storage/grpc_storage.py +214 -0
  109. digitalkin/services/storage/storage_strategy.py +273 -0
  110. digitalkin/services/user_profile/__init__.py +12 -0
  111. digitalkin/services/user_profile/default_user_profile.py +55 -0
  112. digitalkin/services/user_profile/grpc_user_profile.py +69 -0
  113. digitalkin/services/user_profile/user_profile_strategy.py +40 -0
  114. digitalkin/utils/__init__.py +29 -0
  115. digitalkin/utils/arg_parser.py +92 -0
  116. digitalkin/utils/development_mode_action.py +51 -0
  117. digitalkin/utils/dynamic_schema.py +483 -0
  118. digitalkin/utils/llm_ready_schema.py +75 -0
  119. digitalkin/utils/package_discover.py +357 -0
  120. digitalkin-0.3.2.dev2.dist-info/METADATA +602 -0
  121. digitalkin-0.3.2.dev2.dist-info/RECORD +131 -0
  122. digitalkin-0.3.2.dev2.dist-info/WHEEL +5 -0
  123. digitalkin-0.3.2.dev2.dist-info/licenses/LICENSE +430 -0
  124. digitalkin-0.3.2.dev2.dist-info/top_level.txt +4 -0
  125. modules/__init__.py +0 -0
  126. modules/cpu_intensive_module.py +280 -0
  127. modules/dynamic_setup_module.py +338 -0
  128. modules/minimal_llm_module.py +347 -0
  129. modules/text_transform_module.py +203 -0
  130. services/filesystem_module.py +200 -0
  131. 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
+ """