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,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)