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,273 @@
|
|
|
1
|
+
"""This module contains the abstract base class for storage strategies."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Literal, TypeGuard
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from digitalkin.services.base_strategy import BaseStrategy
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StorageServiceError(Exception):
|
|
15
|
+
"""Base exception for Setup service errors."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DataType(Enum):
|
|
19
|
+
"""Enum defining the types of data that can be stored."""
|
|
20
|
+
|
|
21
|
+
OUTPUT = "OUTPUT"
|
|
22
|
+
VIEW = "VIEW"
|
|
23
|
+
LOGS = "LOGS"
|
|
24
|
+
OTHER = "OTHER"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StorageRecord(BaseModel):
|
|
28
|
+
"""A single record stored in a collection, with metadata."""
|
|
29
|
+
|
|
30
|
+
mission_id: str = Field(..., description="ID of the mission (bucket) this doc belongs to")
|
|
31
|
+
collection: str = Field(..., description="Logical collection name")
|
|
32
|
+
record_id: str = Field(..., description="Unique ID of this record in its collection")
|
|
33
|
+
data_type: DataType = Field(default=DataType.OUTPUT, description="Category of the data of this record")
|
|
34
|
+
data: BaseModel = Field(..., description="The typed payload of this record")
|
|
35
|
+
creation_date: datetime.datetime | None = Field(default=None, description="When this record was first created")
|
|
36
|
+
update_date: datetime.datetime | None = Field(default=None, description="When this record was last modified")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StorageStrategy(BaseStrategy, ABC):
|
|
40
|
+
"""Define CRUD + list/remove-collection against a collection/record store."""
|
|
41
|
+
|
|
42
|
+
def _validate_data(self, collection: str, data: dict[str, Any]) -> BaseModel:
|
|
43
|
+
"""Validate data against the model schema for the given key.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
collection: The unique name for the record type
|
|
47
|
+
data: The data to validate
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
A validated model instance
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If the key has no associated model or validation fails
|
|
54
|
+
"""
|
|
55
|
+
model_cls = self.config.get(collection)
|
|
56
|
+
if not model_cls:
|
|
57
|
+
msg = f"No schema registered for collection '{collection}'"
|
|
58
|
+
raise ValueError(msg)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
return model_cls.model_validate(data)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
msg = f"Validation failed for '{collection}': {e!s}"
|
|
64
|
+
raise ValueError(msg) from e
|
|
65
|
+
|
|
66
|
+
def _create_storage_record(
|
|
67
|
+
self,
|
|
68
|
+
collection: str,
|
|
69
|
+
record_id: str,
|
|
70
|
+
validated_data: BaseModel,
|
|
71
|
+
data_type: DataType,
|
|
72
|
+
) -> StorageRecord:
|
|
73
|
+
"""Create a storage record with metadata.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
collection: The unique name for the record type
|
|
77
|
+
record_id: The unique ID for the record
|
|
78
|
+
validated_data: The validated data model
|
|
79
|
+
data_type: The type of data
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A complete storage record with metadata
|
|
83
|
+
"""
|
|
84
|
+
return StorageRecord(
|
|
85
|
+
mission_id=self.mission_id,
|
|
86
|
+
collection=collection,
|
|
87
|
+
record_id=record_id,
|
|
88
|
+
data=validated_data,
|
|
89
|
+
data_type=data_type,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _is_valid_data_type_name(value: str) -> TypeGuard[str]:
|
|
94
|
+
return value in DataType.__members__
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def _store(self, record: StorageRecord) -> StorageRecord:
|
|
98
|
+
"""Store a new record in the storage.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
record: The record to store
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The ID of the created record
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def _read(self, collection: str, record_id: str) -> StorageRecord | None:
|
|
109
|
+
"""Get records from storage by key.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
collection: The unique name to retrieve data for
|
|
113
|
+
record_id: The unique ID of the record
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
A storage record with validated data
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
def _update(self, collection: str, record_id: str, data: BaseModel) -> StorageRecord | None:
|
|
121
|
+
"""Overwrite an existing record's payload.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
collection: The unique name for the record type
|
|
125
|
+
record_id: The unique ID of the record
|
|
126
|
+
data: The new data to store
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
StorageRecord: The modified record
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def _remove(self, collection: str, record_id: str) -> bool:
|
|
134
|
+
"""Delete a record from the storage.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
collection: The unique name for the record type
|
|
138
|
+
record_id: The unique ID of the record
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if the deletion was successful, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def _list(self, collection: str) -> list[StorageRecord]:
|
|
146
|
+
"""List all records in a collection.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
collection: The unique name for the record type
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
A list of storage records
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def _remove_collection(self, collection: str) -> bool:
|
|
157
|
+
"""Delete all records in a collection.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
collection: The unique name for the record type
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if the deletion was successful, False otherwise
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
mission_id: str,
|
|
169
|
+
setup_id: str,
|
|
170
|
+
setup_version_id: str,
|
|
171
|
+
config: dict[str, type[BaseModel]],
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Initialize the storage strategy.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
mission_id: The ID of the mission this strategy is associated with
|
|
177
|
+
setup_id: The ID of the setup
|
|
178
|
+
setup_version_id: The ID of the setup version
|
|
179
|
+
config: A dictionary mapping names to Pydantic model classes
|
|
180
|
+
"""
|
|
181
|
+
super().__init__(mission_id, setup_id, setup_version_id)
|
|
182
|
+
# Schema configuration mapping keys to model classes
|
|
183
|
+
self.config: dict[str, type[BaseModel]] = config
|
|
184
|
+
|
|
185
|
+
def store(
|
|
186
|
+
self,
|
|
187
|
+
collection: str,
|
|
188
|
+
record_id: str | None,
|
|
189
|
+
data: dict[str, Any],
|
|
190
|
+
data_type: Literal["OUTPUT", "VIEW", "LOGS", "OTHER"] = "OUTPUT",
|
|
191
|
+
) -> StorageRecord:
|
|
192
|
+
"""Store a new record in the storage.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
collection: The unique name for the record type
|
|
196
|
+
record_id: The unique ID for the record (optional)
|
|
197
|
+
data: The data to store
|
|
198
|
+
data_type: The type of data being stored (default: OUTPUT)
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The ID of the created record
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: If the data type is invalid or if validation fails
|
|
205
|
+
"""
|
|
206
|
+
if not self._is_valid_data_type_name(data_type):
|
|
207
|
+
msg = f"Invalid data type '{data_type}'. Must be one of {list(DataType.__members__.keys())}"
|
|
208
|
+
raise ValueError(msg)
|
|
209
|
+
record_id = record_id or uuid4().hex
|
|
210
|
+
data_type_enum = DataType[data_type]
|
|
211
|
+
validated_data = self._validate_data(collection, {**data, "mission_id": self.mission_id})
|
|
212
|
+
record = self._create_storage_record(collection, record_id, validated_data, data_type_enum)
|
|
213
|
+
return self._store(record)
|
|
214
|
+
|
|
215
|
+
def read(self, collection: str, record_id: str) -> StorageRecord | None:
|
|
216
|
+
"""Get records from storage by key.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
collection: The unique name to retrieve data for
|
|
220
|
+
record_id: The unique ID of the record
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A storage record with validated data
|
|
224
|
+
"""
|
|
225
|
+
return self._read(collection, record_id)
|
|
226
|
+
|
|
227
|
+
def update(self, collection: str, record_id: str, data: dict[str, Any]) -> StorageRecord | None:
|
|
228
|
+
"""Validate & overwrite an existing record.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
collection: The unique name for the record type
|
|
232
|
+
record_id: The unique ID of the record
|
|
233
|
+
data: The new data to store
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
StorageRecord: The modified record
|
|
237
|
+
"""
|
|
238
|
+
validated_data = self._validate_data(collection, data)
|
|
239
|
+
return self._update(collection, record_id, validated_data)
|
|
240
|
+
|
|
241
|
+
def remove(self, collection: str, record_id: str) -> bool:
|
|
242
|
+
"""Delete a record from the storage.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
collection: The unique name for the record type
|
|
246
|
+
record_id: The unique ID of the record
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if the deletion was successful, False otherwise
|
|
250
|
+
"""
|
|
251
|
+
return self._remove(collection, record_id)
|
|
252
|
+
|
|
253
|
+
def list(self, collection: str) -> list[StorageRecord]:
|
|
254
|
+
"""Get all records within a collection.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
collection: The unique name for the record type
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
A list of storage records
|
|
261
|
+
"""
|
|
262
|
+
return self._list(collection)
|
|
263
|
+
|
|
264
|
+
def remove_collection(self, collection: str) -> bool:
|
|
265
|
+
"""Wipe a record clean.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
collection: The unique name for the record type
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if the deletion was successful, False otherwise
|
|
272
|
+
"""
|
|
273
|
+
return self._remove_collection(collection)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""UserProfile service package."""
|
|
2
|
+
|
|
3
|
+
from digitalkin.services.user_profile.default_user_profile import DefaultUserProfile
|
|
4
|
+
from digitalkin.services.user_profile.grpc_user_profile import GrpcUserProfile
|
|
5
|
+
from digitalkin.services.user_profile.user_profile_strategy import UserProfileServiceError, UserProfileStrategy
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DefaultUserProfile",
|
|
9
|
+
"GrpcUserProfile",
|
|
10
|
+
"UserProfileServiceError",
|
|
11
|
+
"UserProfileStrategy",
|
|
12
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Default user profile implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from digitalkin.logger import logger
|
|
6
|
+
from digitalkin.services.user_profile.user_profile_strategy import (
|
|
7
|
+
UserProfileServiceError,
|
|
8
|
+
UserProfileStrategy,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DefaultUserProfile(UserProfileStrategy):
|
|
13
|
+
"""Default user profile strategy with in-memory storage."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
mission_id: str,
|
|
18
|
+
setup_id: str,
|
|
19
|
+
setup_version_id: str,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Initialize the strategy.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
mission_id: The ID of the mission this strategy is associated with
|
|
25
|
+
setup_id: The ID of the setup
|
|
26
|
+
setup_version_id: The ID of the setup version
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(mission_id=mission_id, setup_id=setup_id, setup_version_id=setup_version_id)
|
|
29
|
+
self.db: dict[str, dict[str, Any]] = {}
|
|
30
|
+
|
|
31
|
+
def get_user_profile(self) -> dict[str, Any]:
|
|
32
|
+
"""Get user profile from in-memory storage.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
dict[str, Any]: User profile data
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
UserProfileServiceError: If the user profile is not found
|
|
39
|
+
"""
|
|
40
|
+
if self.mission_id not in self.db:
|
|
41
|
+
msg = f"User profile for mission {self.mission_id} not found in the database."
|
|
42
|
+
logger.warning(msg)
|
|
43
|
+
raise UserProfileServiceError(msg)
|
|
44
|
+
|
|
45
|
+
logger.debug(f"Retrieved user profile for mission_id: {self.mission_id}")
|
|
46
|
+
return self.db[self.mission_id]
|
|
47
|
+
|
|
48
|
+
def add_user_profile(self, user_profile_data: dict[str, Any]) -> None:
|
|
49
|
+
"""Add a user profile to the in-memory database (helper for testing).
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
user_profile_data: Dictionary containing user profile data
|
|
53
|
+
"""
|
|
54
|
+
self.db[self.mission_id] = user_profile_data
|
|
55
|
+
logger.debug(f"Added user profile for mission_id: {self.mission_id}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Digital Kin UserProfile Service gRPC Client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from agentic_mesh_protocol.user_profile.v1 import (
|
|
6
|
+
user_profile_pb2,
|
|
7
|
+
user_profile_service_pb2_grpc,
|
|
8
|
+
)
|
|
9
|
+
from google.protobuf import json_format
|
|
10
|
+
|
|
11
|
+
from digitalkin.grpc_servers.utils.grpc_client_wrapper import GrpcClientWrapper
|
|
12
|
+
from digitalkin.grpc_servers.utils.grpc_error_handler import GrpcErrorHandlerMixin
|
|
13
|
+
from digitalkin.logger import logger
|
|
14
|
+
from digitalkin.models.grpc_servers.models import ClientConfig
|
|
15
|
+
from digitalkin.services.user_profile.user_profile_strategy import UserProfileServiceError, UserProfileStrategy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GrpcUserProfile(UserProfileStrategy, GrpcClientWrapper, GrpcErrorHandlerMixin):
|
|
19
|
+
"""This class implements the gRPC user profile service."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
mission_id: str,
|
|
24
|
+
setup_id: str,
|
|
25
|
+
setup_version_id: str,
|
|
26
|
+
client_config: ClientConfig,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize the user profile service.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
mission_id: The ID of the mission this strategy is associated with
|
|
32
|
+
setup_id: The ID of the setup
|
|
33
|
+
setup_version_id: The ID of the setup version
|
|
34
|
+
client_config: Client configuration for gRPC connection
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(mission_id=mission_id, setup_id=setup_id, setup_version_id=setup_version_id)
|
|
37
|
+
channel = self._init_channel(client_config)
|
|
38
|
+
self.stub = user_profile_service_pb2_grpc.UserProfileServiceStub(channel)
|
|
39
|
+
logger.debug("Channel client 'UserProfile' initialized successfully")
|
|
40
|
+
|
|
41
|
+
def get_user_profile(self) -> dict[str, Any]:
|
|
42
|
+
"""Get user profile by mission_id (which maps to user_id).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict[str, Any]: User profile data
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
UserProfileServiceError: If the user profile cannot be retrieved
|
|
49
|
+
ServerError: If gRPC operation fails
|
|
50
|
+
"""
|
|
51
|
+
with self.handle_grpc_errors("GetUserProfile", UserProfileServiceError):
|
|
52
|
+
# mission_id typically contains user context
|
|
53
|
+
request = user_profile_pb2.GetUserProfileRequest(mission_id=self.mission_id)
|
|
54
|
+
response = self.exec_grpc_query("GetUserProfile", request)
|
|
55
|
+
|
|
56
|
+
if not response.success:
|
|
57
|
+
msg = f"Failed to get user profile for mission_id: {self.mission_id}"
|
|
58
|
+
logger.error(msg)
|
|
59
|
+
raise UserProfileServiceError(msg)
|
|
60
|
+
|
|
61
|
+
# Convert proto to dict
|
|
62
|
+
user_profile_dict = json_format.MessageToDict(
|
|
63
|
+
response.user_profile,
|
|
64
|
+
preserving_proto_field_name=True,
|
|
65
|
+
always_print_fields_with_no_presence=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger.debug(f"Retrieved user profile for mission_id: {self.mission_id}")
|
|
69
|
+
return user_profile_dict
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""This module contains the abstract base class for UserProfile strategies."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from digitalkin.services.base_strategy import BaseStrategy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserProfileServiceError(Exception):
|
|
10
|
+
"""Base exception for UserProfile service errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UserProfileStrategy(BaseStrategy, ABC):
|
|
14
|
+
"""Abstract base class for UserProfile strategies."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
mission_id: str,
|
|
19
|
+
setup_id: str,
|
|
20
|
+
setup_version_id: str,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Initialize the strategy.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
mission_id: The ID of the mission this strategy is associated with
|
|
26
|
+
setup_id: The ID of the setup
|
|
27
|
+
setup_version_id: The ID of the setup version this strategy is associated with
|
|
28
|
+
"""
|
|
29
|
+
super().__init__(mission_id, setup_id, setup_version_id)
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def get_user_profile(self) -> dict[str, Any]:
|
|
33
|
+
"""Get user profile data.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
dict[str, Any]: User profile data
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
UserProfileServiceError: If the user profile cannot be retrieved
|
|
40
|
+
"""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""General utils folder."""
|
|
2
|
+
|
|
3
|
+
from digitalkin.utils.dynamic_schema import (
|
|
4
|
+
DEFAULT_TIMEOUT,
|
|
5
|
+
DynamicField,
|
|
6
|
+
Fetcher,
|
|
7
|
+
ResolveResult,
|
|
8
|
+
get_dynamic_metadata,
|
|
9
|
+
get_fetchers,
|
|
10
|
+
has_dynamic,
|
|
11
|
+
resolve,
|
|
12
|
+
resolve_safe,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Alias for cleaner API: `Dynamic` is shorter than `DynamicField`
|
|
16
|
+
Dynamic = DynamicField
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DEFAULT_TIMEOUT",
|
|
20
|
+
"Dynamic",
|
|
21
|
+
"DynamicField",
|
|
22
|
+
"Fetcher",
|
|
23
|
+
"ResolveResult",
|
|
24
|
+
"get_dynamic_metadata",
|
|
25
|
+
"get_fetchers",
|
|
26
|
+
"has_dynamic",
|
|
27
|
+
"resolve",
|
|
28
|
+
"resolve_safe",
|
|
29
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""ArgParser and Action classes to ease command lines arguments settings."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from argparse import ArgumentParser, Namespace, _HelpAction, _SubParsersAction # noqa: PLC2701
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from digitalkin.logger import logger
|
|
9
|
+
|
|
10
|
+
logger.setLevel(logging.INFO)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ArgParser:
|
|
14
|
+
"""ArgParse Abstract class to join all argparse argument in the same parser.
|
|
15
|
+
|
|
16
|
+
Custom help display to allow multiple parser and subparser help message.
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
--------
|
|
20
|
+
Inherit this class in your base class.
|
|
21
|
+
Override '_add_parser_args', '_add_exclusive_args' and '_add_subparser_args'.
|
|
22
|
+
|
|
23
|
+
class WindowHandler(ArgParser):
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _add_screen_parser_args(parser) -> None:
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-f", "--fps", type=int, default=60, help="Screen FPS", dest="fps"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _add_media_parser_args(self, parser) -> None:
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-w",
|
|
34
|
+
"--workers",
|
|
35
|
+
type=int,
|
|
36
|
+
default=3,
|
|
37
|
+
help="Number of worker processing media in background",
|
|
38
|
+
dest="media_worker_count"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def _add_parser_args(self, parser) -> None:
|
|
42
|
+
super()._add_parser_args(parser)
|
|
43
|
+
self._add_screen_parser_args(parser)
|
|
44
|
+
self._add_media_parser_args(parser)
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
# init the parser
|
|
48
|
+
super().__init__()
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
args: Namespace
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
Override methods
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
class HelpAction(_HelpAction):
|
|
58
|
+
"""Custom HelpAction to display subparsers helps too."""
|
|
59
|
+
|
|
60
|
+
def __call__(
|
|
61
|
+
self,
|
|
62
|
+
parser: ArgumentParser,
|
|
63
|
+
namespace: Namespace, # noqa: ARG002
|
|
64
|
+
values: str | Sequence[Any] | None, # noqa: ARG002
|
|
65
|
+
option_string: str | None = None, # noqa: ARG002
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Override the HelpActions as it doesn't handle subparser well."""
|
|
68
|
+
parser.print_help()
|
|
69
|
+
subparsers_actions = [action for action in parser._actions if isinstance(action, _SubParsersAction)] # noqa: SLF001
|
|
70
|
+
for subparsers_action in subparsers_actions:
|
|
71
|
+
for choice, subparser in subparsers_action.choices.items():
|
|
72
|
+
logger.info("Subparser '%s':\n%s", choice, subparser.format_help())
|
|
73
|
+
parser.exit()
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
Private methods
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def _add_parser_args(self, parser: ArgumentParser) -> None:
|
|
80
|
+
parser.add_argument("-h", "--help", action=self.HelpAction, help="help usage")
|
|
81
|
+
|
|
82
|
+
def _add_exclusive_args(self, parser: ArgumentParser) -> None: ...
|
|
83
|
+
|
|
84
|
+
def _add_subparser_args(self, parser: ArgumentParser) -> None: ...
|
|
85
|
+
|
|
86
|
+
def __init__(self, prog: str = "PROG") -> None:
|
|
87
|
+
"""Create prser and call abstract methods."""
|
|
88
|
+
self.parser = ArgumentParser(prog=prog, conflict_handler="resolve", add_help=False)
|
|
89
|
+
self._add_parser_args(self.parser)
|
|
90
|
+
self._add_exclusive_args(self.parser)
|
|
91
|
+
self._add_subparser_args(self.parser)
|
|
92
|
+
self.args, _ = self.parser.parse_known_args()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""ArgParser and Action classes to ease command lines arguments settings."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from argparse import Action, ArgumentParser, Namespace
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from digitalkin.logger import logger
|
|
10
|
+
from digitalkin.services.services_models import ServicesMode
|
|
11
|
+
|
|
12
|
+
logger.setLevel(logging.INFO)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DevelopmentModeMappingAction(Action):
|
|
16
|
+
"""ArgParse Action to map an environment variable to a ServicesMode enum."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
env_var: str,
|
|
21
|
+
required: bool = True, # noqa: FBT001, FBT002
|
|
22
|
+
default: str | None = None,
|
|
23
|
+
**kwargs: dict[str, Any],
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Initialize the DevelopmentModeMappingAction."""
|
|
26
|
+
default = ServicesMode(os.environ.get(env_var, default))
|
|
27
|
+
|
|
28
|
+
if required and default:
|
|
29
|
+
required = False
|
|
30
|
+
super().__init__(default=default, required=required, **kwargs) # type: ignore
|
|
31
|
+
|
|
32
|
+
def __call__(
|
|
33
|
+
self,
|
|
34
|
+
parser: ArgumentParser, # noqa: ARG002
|
|
35
|
+
namespace: Namespace,
|
|
36
|
+
values: str | Sequence[Any] | None,
|
|
37
|
+
option_string: str | None = None, # noqa: ARG002
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Set the attribute to the corresponding class.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
TypeError: if the value is not a string.
|
|
43
|
+
"""
|
|
44
|
+
# Check if the value is a string and convert it to lowercase
|
|
45
|
+
if isinstance(values, str):
|
|
46
|
+
values = values.lower()
|
|
47
|
+
else:
|
|
48
|
+
msg = "values must be a string"
|
|
49
|
+
raise TypeError(msg)
|
|
50
|
+
mode = ServicesMode(values)
|
|
51
|
+
setattr(namespace, self.dest, mode)
|