digitalkin 0.1.1__py3-none-any.whl → 0.2.0__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 (77) 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 +124 -0
  6. base_server/server_async_secure.py +142 -0
  7. base_server/server_sync_insecure.py +102 -0
  8. base_server/server_sync_secure.py +121 -0
  9. digitalkin/__init__.py +1 -11
  10. digitalkin/__version__.py +1 -4
  11. digitalkin/{grpc → grpc_servers}/__init__.py +1 -13
  12. digitalkin/{grpc → grpc_servers}/_base_server.py +3 -3
  13. digitalkin/{grpc → grpc_servers}/module_server.py +30 -12
  14. digitalkin/{grpc → grpc_servers}/module_servicer.py +30 -14
  15. digitalkin/{grpc → grpc_servers}/registry_server.py +6 -4
  16. digitalkin/{grpc → grpc_servers}/registry_servicer.py +8 -2
  17. digitalkin/{grpc → grpc_servers}/utils/factory.py +6 -4
  18. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +68 -0
  19. digitalkin/{grpc → grpc_servers}/utils/models.py +1 -1
  20. digitalkin/models/__init__.py +1 -4
  21. digitalkin/models/module/__init__.py +8 -2
  22. digitalkin/models/module/module_types.py +10 -0
  23. digitalkin/models/services/__init__.py +0 -5
  24. digitalkin/modules/__init__.py +3 -3
  25. digitalkin/modules/_base_module.py +64 -27
  26. digitalkin/modules/archetype_module.py +2 -6
  27. digitalkin/modules/job_manager.py +46 -28
  28. digitalkin/modules/tool_module.py +3 -7
  29. digitalkin/modules/trigger_module.py +2 -7
  30. digitalkin/services/__init__.py +7 -9
  31. digitalkin/services/agent/__init__.py +2 -2
  32. digitalkin/services/agent/agent_strategy.py +3 -6
  33. digitalkin/services/agent/default_agent.py +1 -4
  34. digitalkin/services/base_strategy.py +18 -0
  35. digitalkin/services/cost/__init__.py +4 -3
  36. digitalkin/services/cost/cost_strategy.py +35 -5
  37. digitalkin/services/cost/default_cost.py +22 -5
  38. digitalkin/services/cost/grpc_cost.py +81 -0
  39. digitalkin/services/filesystem/__init__.py +4 -3
  40. digitalkin/services/filesystem/default_filesystem.py +197 -17
  41. digitalkin/services/filesystem/filesystem_strategy.py +54 -15
  42. digitalkin/services/filesystem/grpc_filesystem.py +209 -0
  43. digitalkin/services/identity/__init__.py +2 -2
  44. digitalkin/services/identity/default_identity.py +1 -1
  45. digitalkin/services/identity/identity_strategy.py +3 -1
  46. digitalkin/services/registry/__init__.py +2 -2
  47. digitalkin/services/registry/default_registry.py +1 -4
  48. digitalkin/services/registry/registry_strategy.py +3 -6
  49. digitalkin/services/services_config.py +176 -0
  50. digitalkin/services/services_models.py +61 -0
  51. digitalkin/services/setup/default_setup.py +222 -0
  52. digitalkin/services/setup/grpc_setup.py +307 -0
  53. digitalkin/services/setup/setup_strategy.py +145 -0
  54. digitalkin/services/snapshot/__init__.py +2 -2
  55. digitalkin/services/snapshot/default_snapshot.py +1 -1
  56. digitalkin/services/snapshot/snapshot_strategy.py +3 -4
  57. digitalkin/services/storage/__init__.py +4 -3
  58. digitalkin/services/storage/default_storage.py +184 -57
  59. digitalkin/services/storage/grpc_storage.py +76 -170
  60. digitalkin/services/storage/storage_strategy.py +195 -24
  61. digitalkin/utils/arg_parser.py +16 -17
  62. {digitalkin-0.1.1.dist-info → digitalkin-0.2.0.dist-info}/METADATA +8 -7
  63. digitalkin-0.2.0.dist-info/RECORD +78 -0
  64. {digitalkin-0.1.1.dist-info → digitalkin-0.2.0.dist-info}/WHEEL +1 -1
  65. digitalkin-0.2.0.dist-info/top_level.txt +3 -0
  66. modules/__init__.py +0 -0
  67. modules/minimal_llm_module.py +162 -0
  68. modules/storage_module.py +187 -0
  69. modules/text_transform_module.py +201 -0
  70. digitalkin/services/default_service.py +0 -13
  71. digitalkin/services/development_service.py +0 -10
  72. digitalkin/services/service_provider.py +0 -27
  73. digitalkin-0.1.1.dist-info/RECORD +0 -59
  74. digitalkin-0.1.1.dist-info/top_level.txt +0 -1
  75. /digitalkin/{grpc → grpc_servers}/utils/exceptions.py +0 -0
  76. /digitalkin/{grpc → grpc_servers}/utils/types.py +0 -0
  77. {digitalkin-0.1.1.dist-info → digitalkin-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,12 +3,15 @@
3
3
  import asyncio
4
4
  import uuid
5
5
  from argparse import ArgumentParser, Namespace
6
- from collections.abc import Callable
6
+ from collections.abc import Callable, Coroutine
7
7
  from typing import Any
8
8
 
9
9
  from digitalkin.logger import logger
10
10
  from digitalkin.models import ModuleStatus
11
+ from digitalkin.models.module import InputModelT, OutputModelT, SetupModelT
11
12
  from digitalkin.modules._base_module import BaseModule
13
+ from digitalkin.services.services_config import ServicesConfig
14
+ from digitalkin.services.services_models import ServicesMode
12
15
  from digitalkin.utils.arg_parser import ArgParser, DevelopmentModeMappingAction
13
16
 
14
17
 
@@ -17,22 +20,43 @@ class JobManager(ArgParser):
17
20
 
18
21
  args: Namespace
19
22
 
20
- def _add_parser_args(self, parser: ArgumentParser) -> None:
21
- class_mapping = {
22
- "local": self.module_class.local_services,
23
- "development": self.module_class.dev_services,
24
- }
23
+ @staticmethod
24
+ async def _job_specific_callback(
25
+ callback: Callable[[str, OutputModelT], Coroutine[Any, Any, None]], job_id: str
26
+ ) -> Callable[[OutputModelT], Coroutine[Any, Any, None]]:
27
+ """Return a callback function for the job.
28
+
29
+ Args:
30
+ callback: Callback function to be called when the job is done
31
+ job_id: Identifiant du module
32
+
33
+ Returns:
34
+ Callable: Callback function
35
+ """
25
36
 
37
+ def callback_wrapper(output_data: OutputModelT) -> Coroutine[Any, Any, None]:
38
+ """Wrapper for the callback function.
39
+
40
+ Args:
41
+ output_data: Output data of the job
42
+
43
+ Returns:
44
+ Coroutine: Callback function
45
+ """
46
+ return callback(job_id, output_data)
47
+
48
+ return callback_wrapper
49
+
50
+ def _add_parser_args(self, parser: ArgumentParser) -> None:
26
51
  super()._add_parser_args(parser)
27
52
  parser.add_argument(
28
53
  "-d",
29
54
  "--dev-mode",
30
- env_var="SERVICE_PROVIDER",
31
- class_mapping=class_mapping,
32
- choices=class_mapping.keys(),
55
+ env_var="SERVICE_MODE",
56
+ choices=ServicesMode.__members__,
33
57
  default="local",
34
58
  action=DevelopmentModeMappingAction,
35
- dest="service_providers",
59
+ dest="services_mode",
36
60
  help="Define Module Service configurations for endpoints",
37
61
  )
38
62
 
@@ -43,25 +67,18 @@ class JobManager(ArgParser):
43
67
  self._lock = asyncio.Lock()
44
68
  super().__init__()
45
69
 
46
- explicit_fields = {
47
- name: self.args.service_providers.__dict__[name]
48
- for name in self.args.service_providers.__class_vars__
49
- if name in self.args.service_providers.__dict__
50
- }
51
-
52
- # services are now available as class vars.
53
- # init the services provided allowing cold start during module creation
54
- for service_name in explicit_fields:
55
- service_type = getattr(self.args.service_providers, service_name)
56
- setattr(self.module_class, service_name, service_type)
70
+ services_config = ServicesConfig(
71
+ services_config_strategies=self.module_class.services_config_strategies,
72
+ services_config_params=self.module_class.services_config_params,
73
+ mode=self.args.services_mode,
74
+ )
75
+ setattr(self.module_class, "services_config", services_config)
57
76
 
58
77
  async def create_job( # noqa: D417
59
78
  self,
60
- input_data: dict[str, Any],
61
- setup_data: dict[str, Any],
62
- callback: Callable,
63
- *args: tuple,
64
- **kwargs: dict,
79
+ input_data: InputModelT,
80
+ setup_data: SetupModelT,
81
+ callback: Callable[[str, OutputModelT], Coroutine[Any, Any, None]],
65
82
  ) -> tuple[str, BaseModule]:
66
83
  """Start new module job in background (asyncio).
67
84
 
@@ -74,12 +91,13 @@ class JobManager(ArgParser):
74
91
  str: job_id of the module entity
75
92
  """
76
93
  job_id = str(uuid.uuid4())
94
+ mission_id = "missions:test_demo"
77
95
  """TODO: check uniqueness of the job_id"""
78
96
  # Création et démarrage du module
79
- module = self.module_class(job_id, *args, **kwargs) # type: ignore
97
+ module = self.module_class(job_id, mission_id=mission_id)
80
98
  self.modules[job_id] = module
81
99
  try:
82
- await module.start(input_data, setup_data, callback)
100
+ await module.start(input_data, setup_data, await JobManager._job_specific_callback(callback, job_id))
83
101
  logger.info("Module %s (%s) started successfully", job_id, module.name)
84
102
  except Exception:
85
103
  # En cas d'erreur, supprimer le module du gestionnaire
@@ -2,13 +2,9 @@
2
2
 
3
3
  from abc import ABC
4
4
 
5
- from ._base_module import BaseModule
5
+ from digitalkin.models.module import InputModelT, OutputModelT, SecretModelT, SetupModelT
6
+ from digitalkin.modules._base_module import BaseModule # type: ignore
6
7
 
7
8
 
8
- class ToolModule(BaseModule, ABC):
9
+ class ToolModule(BaseModule[InputModelT, OutputModelT, SetupModelT, SecretModelT], ABC):
9
10
  """ToolModule extends BaseModule to implement specific module types."""
10
-
11
- def __init__(self, metadata):
12
- """Initialize the module with the given metadata."""
13
- super().__init__(metadata)
14
- self.capabilities = ["tool"]
@@ -2,13 +2,8 @@
2
2
 
3
3
  from abc import ABC
4
4
 
5
- from ._base_module import BaseModule
5
+ from digitalkin.modules._base_module import BaseModule, InputModelT, OutputModelT, SecretModelT, SetupModelT
6
6
 
7
7
 
8
- class TriggerModule(BaseModule, ABC):
8
+ class TriggerModule(BaseModule[InputModelT, OutputModelT, SetupModelT, SecretModelT], ABC):
9
9
  """TriggerModule extends BaseModule to implement specific module types."""
10
-
11
- def __init__(self, metadata):
12
- """Initialize the module with the given metadata."""
13
- super().__init__(metadata)
14
- self.capabilities = ["trigger"]
@@ -1,13 +1,12 @@
1
1
  """This package contains the abstract base class for all services."""
2
2
 
3
- from .agent import AgentStrategy, DefaultAgent
4
- from .cost import CostStrategy, DefaultCost
5
- from .filesystem import DefaultFilesystem, FilesystemStrategy
6
- from .identity import DefaultIdentity, IdentityStrategy
7
- from .registry import DefaultRegistry, RegistryStrategy
8
- from .service_provider import ServiceProvider
9
- from .snapshot import DefaultSnapshot, SnapshotStrategy
10
- from .storage import DefaultStorage, StorageStrategy
3
+ from digitalkin.services.agent import AgentStrategy, DefaultAgent
4
+ from digitalkin.services.cost import CostStrategy, DefaultCost
5
+ from digitalkin.services.filesystem import DefaultFilesystem, FilesystemStrategy
6
+ from digitalkin.services.identity import DefaultIdentity, IdentityStrategy
7
+ from digitalkin.services.registry import DefaultRegistry, RegistryStrategy
8
+ from digitalkin.services.snapshot import DefaultSnapshot, SnapshotStrategy
9
+ from digitalkin.services.storage import DefaultStorage, StorageStrategy
11
10
 
12
11
  __all__ = [
13
12
  "AgentStrategy",
@@ -22,7 +21,6 @@ __all__ = [
22
21
  "FilesystemStrategy",
23
22
  "IdentityStrategy",
24
23
  "RegistryStrategy",
25
- "ServiceProvider",
26
24
  "SnapshotStrategy",
27
25
  "StorageStrategy",
28
26
  ]
@@ -1,6 +1,6 @@
1
1
  """This module is responsible for handling the agent services."""
2
2
 
3
- from .agent_strategy import AgentStrategy
4
- from .default_agent import DefaultAgent
3
+ from digitalkin.services.agent.agent_strategy import AgentStrategy
4
+ from digitalkin.services.agent.default_agent import DefaultAgent
5
5
 
6
6
  __all__ = ["AgentStrategy", "DefaultAgent"]
@@ -2,14 +2,11 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
 
5
+ from digitalkin.services.base_strategy import BaseStrategy
5
6
 
6
- class AgentStrategy(ABC):
7
- """Abstract base class for agent strategies."""
8
7
 
9
- @abstractmethod
10
- def __init__(self) -> None:
11
- """Initialize the agent strategy."""
12
- raise NotImplementedError
8
+ class AgentStrategy(BaseStrategy, ABC):
9
+ """Abstract base class for agent strategies."""
13
10
 
14
11
  @abstractmethod
15
12
  def start(self) -> None:
@@ -1,14 +1,11 @@
1
1
  """Default agent implementation for the agent service."""
2
2
 
3
- from .agent_strategy import AgentStrategy
3
+ from digitalkin.services.agent.agent_strategy import AgentStrategy
4
4
 
5
5
 
6
6
  class DefaultAgent(AgentStrategy):
7
7
  """Default agent implementation for the agent service."""
8
8
 
9
- def __init__(self) -> None:
10
- """Initialize the default agent."""
11
-
12
9
  def start(self) -> None:
13
10
  """Start the agent."""
14
11
 
@@ -0,0 +1,18 @@
1
+ """This module contains the abstract base class for storage strategies."""
2
+
3
+ from abc import ABC
4
+
5
+
6
+ class BaseStrategy(ABC):
7
+ """Abstract base class for all strategies.
8
+
9
+ This class defines the interface for all strategies.
10
+ """
11
+
12
+ def __init__(self, mission_id: str) -> None:
13
+ """Initialize the strategy.
14
+
15
+ Args:
16
+ mission_id: The ID of the mission this strategy is associated with
17
+ """
18
+ self.mission_id: str = mission_id
@@ -1,6 +1,7 @@
1
1
  """This module is responsible for handling the cost services."""
2
2
 
3
- from .cost_strategy import CostStrategy
4
- from .default_cost import DefaultCost
3
+ from digitalkin.services.cost.cost_strategy import CostStrategy
4
+ from digitalkin.services.cost.default_cost import DefaultCost
5
+ from digitalkin.services.cost.grpc_cost import GrpcCost
5
6
 
6
- __all__ = ["CostStrategy", "DefaultCost"]
7
+ __all__ = ["CostStrategy", "DefaultCost", "GrpcCost"]
@@ -1,15 +1,45 @@
1
1
  """This module contains the abstract base class for cost strategies."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
+ from enum import Enum, auto
5
+ from typing import Any
4
6
 
7
+ from pydantic import BaseModel
5
8
 
6
- class CostStrategy(ABC):
9
+ from digitalkin.services.base_strategy import BaseStrategy
10
+
11
+
12
+ class CostType(Enum):
13
+ """."""
14
+
15
+ OTHER = auto()
16
+ TOKEN_INPUT = auto()
17
+ TOKEN_OUTPUT = auto()
18
+ API_CALL = auto()
19
+ STORAGE = auto()
20
+ TIME = auto()
21
+
22
+
23
+ class CostData(BaseModel):
24
+ """."""
25
+
26
+ cost: float
27
+ mission_id: str
28
+ name: str
29
+ type: CostType
30
+ unit: str
31
+
32
+
33
+ class CostStrategy(BaseStrategy, ABC):
7
34
  """Abstract base class for cost strategies."""
8
35
 
9
36
  @abstractmethod
10
- def __init__(self) -> None:
11
- """Initialize the cost strategy."""
37
+ def add_cost(self, cost_dict: dict[str, Any]) -> str:
38
+ """Register a new cost."""
39
+
40
+ def __post_init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
41
+ """Allow post init configuration."""
12
42
 
13
43
  @abstractmethod
14
- def register_cost(self, data: dict[str, str]) -> None:
15
- """Register a new cost."""
44
+ def get(self, cost_dict: dict[str, Any]) -> list[CostData]:
45
+ """Get a cost."""
@@ -1,13 +1,30 @@
1
1
  """Default cost."""
2
2
 
3
- from .cost_strategy import CostStrategy
3
+ import logging
4
+ from typing import Any
5
+
6
+ from digitalkin.services.cost.cost_strategy import CostData, CostStrategy
7
+
8
+ logger = logging.getLogger(__name__)
4
9
 
5
10
 
6
11
  class DefaultCost(CostStrategy):
7
12
  """Default cost strategy."""
8
13
 
9
- def __init__(self) -> None:
10
- """Initialize the cost strategy."""
14
+ def add_cost(self, cost_dict: dict[str, Any]) -> str: # noqa: PLR6301
15
+ """Create a new record in the cost database.
16
+
17
+ Returns:
18
+ str: The ID of the new record
19
+ """
20
+ logger.info("Cost added with cost_dict: %s", cost_dict)
21
+ return ""
22
+
23
+ def get(self, cost_dict: dict[str, Any]) -> list[CostData]: # noqa: PLR6301
24
+ """Get records from the database.
11
25
 
12
- def register_cost(self, data: dict[str, str]) -> None:
13
- """Register a new cost."""
26
+ Returns:
27
+ list[CostData]: The list of records
28
+ """
29
+ logger.info("Costs querried with cost_dict: %s", cost_dict)
30
+ return []
@@ -0,0 +1,81 @@
1
+ """This module implements the default Cost strategy."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from digitalkin_proto.digitalkin.cost.v1 import cost_pb2, cost_service_pb2_grpc
7
+ from google.protobuf import json_format
8
+ from pydantic import ValidationError
9
+
10
+ from digitalkin.grpc_servers.utils.grpc_client_wrapper import GrpcClientWrapper
11
+ from digitalkin.grpc_servers.utils.models import ServerConfig
12
+ from digitalkin.services.cost.cost_strategy import CostData, CostStrategy
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class GrpcCost(CostStrategy, GrpcClientWrapper):
18
+ """This class implements the default Cost strategy."""
19
+
20
+ def _get_costs_by_name(self, cost_dict: dict[str, Any]) -> list[CostData]:
21
+ request = cost_pb2.GetCostsByNameRequest(
22
+ mission_id=cost_dict["mission_id"],
23
+ name=cost_dict["name"],
24
+ )
25
+ response = self.exec_grpc_query("GetCostsByName", request)
26
+ return [CostData(**json_format.MessageToDict(cost)) for cost in response.costs]
27
+
28
+ def _get_costs_by_mission(self, cost_dict: dict[str, Any]) -> list[CostData]:
29
+ request = cost_pb2.GetCostsByMissionRequest(mission_id=cost_dict["mission_id"])
30
+ response = self.exec_grpc_query("GetCostsByMission", request)
31
+ return [CostData(**json_format.MessageToDict(cost)) for cost in response.costs]
32
+
33
+ def _get_costs_by_type(self, cost_dict: dict[str, Any]) -> list[CostData]:
34
+ request = cost_pb2.GetCostsByTypeRequest(
35
+ mission_id=cost_dict["mission_id"],
36
+ type=cost_dict["type"],
37
+ )
38
+ response = self.exec_grpc_query("GetCostsBytype", request)
39
+ return [CostData(**json_format.MessageToDict(cost)) for cost in response.costs]
40
+
41
+ def __init__(self, mission_id: str, config: ServerConfig) -> None:
42
+ """Initialize the cost."""
43
+ super().__init__(mission_id)
44
+ channel = self._init_channel(config)
45
+ self.stub = cost_service_pb2_grpc.CostServiceStub(channel)
46
+ logger.info("Channel client 'Cost' initialized succesfully")
47
+
48
+ def add_cost(self, cost_dict: dict[str, Any]) -> str:
49
+ """Create a new record in the cost database.
50
+
51
+ Required arguments:
52
+ data: Object representation of CostData
53
+
54
+ Returns:
55
+ str: The ID of the new record
56
+ """
57
+ try:
58
+ valid_data = CostData.model_validate(cost_dict["data"])
59
+ except ValidationError:
60
+ logger.exception("Validation failed for model StorageData")
61
+ return ""
62
+ except KeyError:
63
+ logger.exception("Missing mandatory 'data' in dict.")
64
+ return ""
65
+
66
+ request = cost_pb2.AddCostRequest(**valid_data.model_dump())
67
+ return self.exec_grpc_query("AddCost", request)
68
+
69
+ def get(self, cost_dict: dict[str, Any]) -> list[CostData]:
70
+ """Get records from the database.
71
+
72
+ Returns:
73
+ list[CostData]: The list of records
74
+ """
75
+ if "mission_id" not in cost_dict:
76
+ return []
77
+ if "name" in cost_dict:
78
+ return self._get_costs_by_name(cost_dict)
79
+ if "type" in cost_dict:
80
+ return self._get_costs_by_type(cost_dict)
81
+ return self._get_costs_by_mission(cost_dict)
@@ -1,6 +1,7 @@
1
1
  """This module is responsible for handling the filesystem services."""
2
2
 
3
- from .default_filesystem import DefaultFilesystem
4
- from .filesystem_strategy import FilesystemStrategy
3
+ from digitalkin.services.filesystem.default_filesystem import DefaultFilesystem
4
+ from digitalkin.services.filesystem.filesystem_strategy import FilesystemStrategy
5
+ from digitalkin.services.filesystem.grpc_filesystem import GrpcFilesystem
5
6
 
6
- __all__ = ["DefaultFilesystem", "FilesystemStrategy"]
7
+ __all__ = ["DefaultFilesystem", "FilesystemStrategy", "GrpcFilesystem"]
@@ -1,29 +1,209 @@
1
1
  """Default filesystem."""
2
2
 
3
- from typing import Any
3
+ import logging
4
+ import os
5
+ import tempfile
6
+ from pathlib import Path
4
7
 
5
- from .filesystem_strategy import FilesystemStrategy
8
+ from digitalkin.services.filesystem.filesystem_strategy import (
9
+ FilesystemData,
10
+ FilesystemServiceError,
11
+ FilesystemStrategy,
12
+ FileType,
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
6
16
 
7
17
 
8
18
  class DefaultFilesystem(FilesystemStrategy):
9
19
  """Default state filesystem strategy."""
10
20
 
11
- def create(self, data: dict[str, Any]) -> str:
12
- """Create a new file in the file system."""
13
- raise NotImplementedError
21
+ def __init__(self, mission_id: str, config: dict[str, str]) -> None:
22
+ """Initialize the default filesystem strategy.
23
+
24
+ Args:
25
+ mission_id: The ID of the mission this strategy is associated with
26
+ config: A dictionary mapping names to Pydantic model classes
27
+ """
28
+ super().__init__(mission_id, config)
29
+ self.temp_root: str = self.config.get("temp_root", "") or tempfile.gettempdir()
30
+ os.makedirs(self.temp_root, exist_ok=True)
31
+ self.db: dict[str, FilesystemData] = {}
32
+
33
+ def _get_kin_context_temp_dir(self, kin_context: str) -> str:
34
+ """Get the temporary directory path for a specific kin_context.
35
+
36
+ Args:
37
+ kin_context: The mission ID or setup ID.
38
+
39
+ Returns:
40
+ str: Path to the kin_context's temporary directory
41
+ """
42
+ # Create a kin_context-specific directory to organize files
43
+ kin_context_dir = os.path.join(self.temp_root, kin_context.replace(":", "_"))
44
+ os.makedirs(kin_context_dir, exist_ok=True)
45
+ return kin_context_dir
46
+
47
+ def upload(self, content: bytes, name: str, file_type: FileType) -> FilesystemData:
48
+ """Create a new file in the file system.
49
+
50
+ Args:
51
+ content: The content of the file to be uploaded
52
+ name: The name of the file to be created
53
+ file_type: The type of data being uploaded
54
+
55
+ Returns:
56
+ FilesystemData: Metadata about the uploaded file
57
+
58
+ Raises:
59
+ FileExistsError: If the file already exists
60
+ FilesystemServiceError: If there is an error during upload
61
+ """
62
+ if self.db.get(name):
63
+ msg = f"File with name {name} already exists."
64
+ logger.error(msg)
65
+ raise FileExistsError(msg)
66
+ try:
67
+ kin_context_dir = self._get_kin_context_temp_dir(self.mission_id)
68
+ file_path = os.path.join(kin_context_dir, name)
69
+ Path(file_path).write_bytes(content)
70
+ url = str(Path(file_path).resolve())
71
+ return FilesystemData(
72
+ kin_context=self.mission_id,
73
+ name=name,
74
+ file_type=file_type,
75
+ url=url,
76
+ )
77
+ except Exception:
78
+ msg = f"Error uploading file {name}"
79
+ logger.exception(msg)
80
+ raise FilesystemServiceError(msg)
81
+
82
+ def get(self, name: str) -> FilesystemData:
83
+ """Get file from the filesystem.
84
+
85
+ Args:
86
+ name: The name of the file to be retrieved
87
+
88
+ Returns:
89
+ FilesystemData: Metadata about the retrieved file
90
+
91
+ Raises:
92
+ FileNotFoundError: If the file does not exist
93
+ FilesystemServiceError: If the file does not exist
94
+ """
95
+ try:
96
+ return self.db[name]
97
+ except KeyError:
98
+ # If the file does not exist in the database, raise an error
99
+ msg = f"File with name {name} does not exist."
100
+ logger.exception(msg)
101
+ raise FileNotFoundError(msg)
102
+ except Exception:
103
+ msg = f"Error getting file {name}"
104
+ logger.exception(msg)
105
+ raise FilesystemServiceError(msg)
106
+
107
+ def update(self, name: str, content: bytes, file_type: FileType) -> FilesystemData:
108
+ """Update files in the filesystem.
109
+
110
+ Args:
111
+ content: The new content of the file
112
+ name: The name of the file to be updated
113
+ file_type: The type of data being updated
114
+
115
+ Returns:
116
+ FilesystemData: Metadata about the updated file
117
+
118
+ Raises:
119
+ FileNotFoundError: If the file does not exist
120
+ FilesystemServiceError: If there is an error during update
121
+ """
122
+ if name not in self.db:
123
+ msg = f"File with name {name} does not exist."
124
+ logger.error(msg)
125
+ raise FileNotFoundError(msg)
126
+ try:
127
+ kin_context_dir = self._get_kin_context_temp_dir(self.mission_id)
128
+ file_path = os.path.join(kin_context_dir, name)
129
+ Path(file_path).write_bytes(content)
130
+ url = str(Path(file_path).resolve())
131
+ file = FilesystemData(
132
+ kin_context=self.mission_id,
133
+ name=name,
134
+ file_type=file_type,
135
+ url=url,
136
+ )
137
+ self.db[name] = file
138
+ except Exception:
139
+ msg = f"Error updating file {name}"
140
+ logger.exception(msg)
141
+ raise FilesystemServiceError(msg)
142
+ else:
143
+ return file
144
+
145
+ def delete(self, name: str) -> bool:
146
+ """Delete files from the filesystem.
147
+
148
+ Args:
149
+ name: The name of the file to be deleted
150
+
151
+ Returns:
152
+ int: 1 if the file was deleted successfully
153
+
154
+ Raises:
155
+ FileNotFoundError: If the file does not exist
156
+ FilesystemServiceError: If there is an error during deletion
157
+ """
158
+ # First check if the file exists in the database
159
+ if name not in self.db:
160
+ msg = f"File with name {name} does not exist in the database."
161
+ logger.error(msg)
162
+ raise FileNotFoundError(msg)
163
+
164
+ # Get the file path
165
+ kin_context_dir = self._get_kin_context_temp_dir(self.mission_id)
166
+ file_path = os.path.join(kin_context_dir, name)
167
+
168
+ # Check if the file exists in the filesystem
169
+ if not os.path.exists(file_path):
170
+ msg = f"File {name} exists in database but not in filesystem at {file_path}."
171
+ logger.error(msg)
172
+ # We could decide to just remove from DB here, but that might hide a larger issue
173
+ # So we're raising a custom error to alert about the inconsistency
174
+ raise FilesystemServiceError(msg)
175
+
176
+ try:
177
+ os.remove(file_path)
178
+ del self.db[name]
179
+ logger.info("File %s successfully deleted.", name)
180
+
181
+ except OSError:
182
+ msg = f"Error deleting file {name} from filesystem"
183
+ logger.exception(msg)
184
+ raise FilesystemServiceError(msg)
185
+ except Exception:
186
+ msg = f"Unexpected error deleting file {name}"
187
+ logger.exception(msg)
188
+ raise FilesystemServiceError(msg)
189
+ else:
190
+ return True
191
+
192
+ def get_all(self) -> list[FilesystemData]:
193
+ """Get all files from the filesystem.
14
194
 
15
- def get(self, data: dict[str, Any]) -> list[dict[str, Any]]:
16
- """Get files from the file system."""
17
- raise NotImplementedError
195
+ Returns:
196
+ list[FilesystemData]: A list of all files in the filesystem
197
+ """
198
+ return list(self.db.values())
18
199
 
19
- def update(self, data: dict[str, Any]) -> int:
20
- """Update files in the file system."""
21
- raise NotImplementedError
200
+ def get_batch(self, names: list[str]) -> dict[str, FilesystemData | None]:
201
+ """Get files from the filesystem.
22
202
 
23
- def delete(self, data: dict[str, Any]) -> int:
24
- """Delete files from the file system."""
25
- raise NotImplementedError
203
+ Args:
204
+ names: The names of the files to be retrieved
26
205
 
27
- def get_all(self) -> list[dict[str, Any]]:
28
- """Get all files from the file system."""
29
- raise NotImplementedError
206
+ Returns:
207
+ dict[FilesystemData | None]: Metadata about the retrieved files
208
+ """
209
+ return {name: self.db.get(name, None) for name in names}