digitalkin 0.1.1__py3-none-any.whl → 0.2.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.
- 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 +124 -0
- base_server/server_async_secure.py +142 -0
- base_server/server_sync_insecure.py +102 -0
- base_server/server_sync_secure.py +121 -0
- digitalkin/__init__.py +1 -11
- digitalkin/__version__.py +1 -4
- digitalkin/{grpc → grpc_servers}/__init__.py +1 -13
- digitalkin/{grpc → grpc_servers}/_base_server.py +3 -3
- digitalkin/{grpc → grpc_servers}/module_server.py +31 -13
- digitalkin/{grpc → grpc_servers}/module_servicer.py +30 -14
- digitalkin/{grpc → grpc_servers}/registry_server.py +13 -8
- digitalkin/{grpc → grpc_servers}/registry_servicer.py +8 -2
- digitalkin/{grpc → grpc_servers}/utils/factory.py +6 -4
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +68 -0
- digitalkin/{grpc → grpc_servers}/utils/models.py +1 -1
- digitalkin/models/__init__.py +1 -4
- digitalkin/models/module/__init__.py +8 -2
- digitalkin/models/module/module_types.py +10 -0
- digitalkin/models/services/__init__.py +0 -5
- digitalkin/modules/__init__.py +3 -3
- digitalkin/modules/_base_module.py +64 -27
- digitalkin/modules/archetype_module.py +2 -6
- digitalkin/modules/job_manager.py +46 -28
- digitalkin/modules/tool_module.py +3 -7
- digitalkin/modules/trigger_module.py +2 -7
- digitalkin/services/__init__.py +7 -9
- digitalkin/services/agent/__init__.py +2 -2
- digitalkin/services/agent/agent_strategy.py +3 -6
- digitalkin/services/agent/default_agent.py +1 -4
- digitalkin/services/base_strategy.py +18 -0
- digitalkin/services/cost/__init__.py +4 -3
- digitalkin/services/cost/cost_strategy.py +35 -5
- digitalkin/services/cost/default_cost.py +22 -5
- digitalkin/services/cost/grpc_cost.py +81 -0
- digitalkin/services/filesystem/__init__.py +4 -3
- digitalkin/services/filesystem/default_filesystem.py +197 -17
- digitalkin/services/filesystem/filesystem_strategy.py +54 -15
- digitalkin/services/filesystem/grpc_filesystem.py +209 -0
- digitalkin/services/identity/__init__.py +2 -2
- digitalkin/services/identity/default_identity.py +1 -1
- digitalkin/services/identity/identity_strategy.py +3 -1
- digitalkin/services/registry/__init__.py +2 -2
- digitalkin/services/registry/default_registry.py +1 -4
- digitalkin/services/registry/registry_strategy.py +3 -6
- digitalkin/services/services_config.py +176 -0
- digitalkin/services/services_models.py +61 -0
- digitalkin/services/setup/default_setup.py +222 -0
- digitalkin/services/setup/grpc_setup.py +307 -0
- digitalkin/services/setup/setup_strategy.py +145 -0
- digitalkin/services/snapshot/__init__.py +2 -2
- digitalkin/services/snapshot/default_snapshot.py +1 -1
- digitalkin/services/snapshot/snapshot_strategy.py +3 -4
- digitalkin/services/storage/__init__.py +4 -3
- digitalkin/services/storage/default_storage.py +184 -57
- digitalkin/services/storage/grpc_storage.py +76 -170
- digitalkin/services/storage/storage_strategy.py +195 -24
- digitalkin/utils/arg_parser.py +16 -17
- {digitalkin-0.1.1.dist-info → digitalkin-0.2.1.dist-info}/METADATA +8 -7
- digitalkin-0.2.1.dist-info/RECORD +78 -0
- {digitalkin-0.1.1.dist-info → digitalkin-0.2.1.dist-info}/WHEEL +1 -1
- digitalkin-0.2.1.dist-info/top_level.txt +3 -0
- modules/__init__.py +0 -0
- modules/minimal_llm_module.py +162 -0
- modules/storage_module.py +187 -0
- modules/text_transform_module.py +201 -0
- digitalkin/services/default_service.py +0 -13
- digitalkin/services/development_service.py +0 -10
- digitalkin/services/service_provider.py +0 -27
- digitalkin-0.1.1.dist-info/RECORD +0 -59
- digitalkin-0.1.1.dist-info/top_level.txt +0 -1
- /digitalkin/{grpc → grpc_servers}/utils/exceptions.py +0 -0
- /digitalkin/{grpc → grpc_servers}/utils/types.py +0 -0
- {digitalkin-0.1.1.dist-info → digitalkin-0.2.1.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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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="
|
|
31
|
-
|
|
32
|
-
choices=class_mapping.keys(),
|
|
55
|
+
env_var="SERVICE_MODE",
|
|
56
|
+
choices=ServicesMode.__members__,
|
|
33
57
|
default="local",
|
|
34
58
|
action=DevelopmentModeMappingAction,
|
|
35
|
-
dest="
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
61
|
-
setup_data:
|
|
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,
|
|
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 .
|
|
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"]
|
digitalkin/services/__init__.py
CHANGED
|
@@ -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 .
|
|
9
|
-
from .
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
11
|
-
"""
|
|
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
|
|
15
|
-
"""
|
|
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
|
-
|
|
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
|
|
10
|
-
"""
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
4
7
|
|
|
5
|
-
from .filesystem_strategy import
|
|
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
|
|
12
|
-
"""
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
195
|
+
Returns:
|
|
196
|
+
list[FilesystemData]: A list of all files in the filesystem
|
|
197
|
+
"""
|
|
198
|
+
return list(self.db.values())
|
|
18
199
|
|
|
19
|
-
def
|
|
20
|
-
"""
|
|
21
|
-
raise NotImplementedError
|
|
200
|
+
def get_batch(self, names: list[str]) -> dict[str, FilesystemData | None]:
|
|
201
|
+
"""Get files from the filesystem.
|
|
22
202
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
raise NotImplementedError
|
|
203
|
+
Args:
|
|
204
|
+
names: The names of the files to be retrieved
|
|
26
205
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
206
|
+
Returns:
|
|
207
|
+
dict[FilesystemData | None]: Metadata about the retrieved files
|
|
208
|
+
"""
|
|
209
|
+
return {name: self.db.get(name, None) for name in names}
|