digitalkin 0.3.2.dev8__py3-none-any.whl → 0.3.2.dev11__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.
digitalkin/__version__.py CHANGED
@@ -5,4 +5,4 @@ from importlib.metadata import PackageNotFoundError, version
5
5
  try:
6
6
  __version__ = version("digitalkin")
7
7
  except PackageNotFoundError:
8
- __version__ = "0.3.2.dev8"
8
+ __version__ = "0.3.2.dev11"
@@ -43,9 +43,9 @@ class GrpcClientWrapper:
43
43
  private_key=private_key,
44
44
  )
45
45
 
46
- return grpc.secure_channel(config.address, channel_credentials, options=config.channel_options)
46
+ return grpc.secure_channel(config.address, channel_credentials, options=config.grpc_options)
47
47
  # Insecure channel
48
- return grpc.insecure_channel(config.address, options=config.channel_options)
48
+ return grpc.insecure_channel(config.address, options=config.grpc_options)
49
49
 
50
50
  def exec_grpc_query(self, query_endpoint: str, request: Any) -> Any: # noqa: ANN401
51
51
  """Execute a gRPC query with from the query's rpc endpoint name.
@@ -65,6 +65,42 @@ class ServerCredentials(BaseModel):
65
65
  return v
66
66
 
67
67
 
68
+ class RetryPolicy(BaseModel):
69
+ """gRPC retry policy configuration for resilient connections.
70
+
71
+ Attributes:
72
+ max_attempts: Maximum retry attempts including the original call
73
+ initial_backoff: Initial backoff duration (e.g., "0.1s")
74
+ max_backoff: Maximum backoff duration (e.g., "10s")
75
+ backoff_multiplier: Multiplier for exponential backoff
76
+ retryable_status_codes: gRPC status codes that trigger retry
77
+ """
78
+
79
+ max_attempts: int = Field(default=5, ge=1, le=10, description="Maximum retry attempts including the original call")
80
+ initial_backoff: str = Field(default="0.1s", description="Initial backoff duration (e.g., '0.1s')")
81
+ max_backoff: str = Field(default="10s", description="Maximum backoff duration (e.g., '10s')")
82
+ backoff_multiplier: float = Field(default=2.0, ge=1.0, description="Multiplier for exponential backoff")
83
+ retryable_status_codes: list[str] = Field(
84
+ default_factory=lambda: ["UNAVAILABLE", "RESOURCE_EXHAUSTED"],
85
+ description="gRPC status codes that trigger retry",
86
+ )
87
+
88
+ model_config = {"extra": "forbid", "frozen": True}
89
+
90
+ def to_service_config_json(self) -> str:
91
+ """Serialize to gRPC service config JSON string.
92
+
93
+ Returns:
94
+ JSON string for grpc.service_config channel option.
95
+ """
96
+ codes = "[" + ",".join(f'"{c}"' for c in self.retryable_status_codes) + "]"
97
+ return (
98
+ f'{{"methodConfig":[{{"name":[{{}}],"retryPolicy":{{"maxAttempts":{self.max_attempts},'
99
+ f'"initialBackoff":"{self.initial_backoff}","maxBackoff":"{self.max_backoff}",'
100
+ f'"backoffMultiplier":{self.backoff_multiplier},"retryableStatusCodes":{codes}}}}}]}}'
101
+ )
102
+
103
+
68
104
  class ClientCredentials(BaseModel):
69
105
  """Model for client credentials in secure mode.
70
106
 
@@ -170,15 +206,47 @@ class ClientConfig(ChannelConfig):
170
206
  security: Security mode (secure/insecure)
171
207
  credentials: Client credentials for secure mode
172
208
  channel_options: Additional channel options
209
+ retry_policy: Retry policy for failed RPCs
173
210
  """
174
211
 
175
212
  credentials: ClientCredentials | None = Field(None, description="Client credentials for secure mode")
213
+ retry_policy: RetryPolicy = Field(default_factory=lambda: RetryPolicy(), description="Retry policy for failed RPCs") # noqa: PLW0108
176
214
  channel_options: list[tuple[str, Any]] = Field(
177
215
  default_factory=lambda: [
178
- ("grpc.max_receive_message_length", 100 * 1024 * 1024), # 100MB
179
- ("grpc.max_send_message_length", 100 * 1024 * 1024), # 100MB
216
+ ("grpc.max_receive_message_length", 100 * 1024 * 1024),
217
+ ("grpc.max_send_message_length", 100 * 1024 * 1024),
218
+ # === DNS Re-resolution (Critical for Container Environments) ===
219
+ # Minimum milliseconds between DNS re-resolution attempts (500 ms)
220
+ # When connection fails, gRPC will re-query DNS after this interval
221
+ # Solves: Container restarts with new IPs causing "No route to host"
222
+ ("grpc.dns_min_time_between_resolutions_ms", 500),
223
+ # Initial delay before first reconnection attempt (1 second)
224
+ ("grpc.initial_reconnect_backoff_ms", 1000),
225
+ # Maximum delay between reconnection attempts (10 seconds)
226
+ # Prevents overwhelming the network during extended outages
227
+ ("grpc.max_reconnect_backoff_ms", 10000),
228
+ # Minimum delay between reconnection attempts (500ms)
229
+ # Ensures rapid recovery for brief network glitches
230
+ ("grpc.min_reconnect_backoff_ms", 500),
231
+ # === Keepalive Settings (Detect Dead Connections) ===
232
+ # Send keepalive ping every 60 seconds when connection is idle
233
+ # Proactively detects dead connections before RPC calls fail
234
+ ("grpc.keepalive_time_ms", 60000),
235
+ # Wait 20 seconds for keepalive response before declaring connection dead
236
+ # Triggers reconnection (with DNS re-resolution) if pong not received
237
+ ("grpc.keepalive_timeout_ms", 20000),
238
+ # Send keepalive pings even when no RPCs are in flight
239
+ # Essential for long-lived connections that may sit idle
240
+ ("grpc.keepalive_permit_without_calls", True),
241
+ # Minimum interval between HTTP/2 pings (30 seconds)
242
+ # Must be >= server's grpc.http2.min_ping_interval_without_data_ms (10s)
243
+ ("grpc.http2.min_time_between_pings_ms", 30000),
244
+ # === Retry Configuration ===
245
+ # Enable automatic retry for failed RPCs (1 = enabled)
246
+ # Works with retryable status codes: UNAVAILABLE, RESOURCE_EXHAUSTED
247
+ ("grpc.enable_retries", 1),
180
248
  ],
181
- description="Additional channel options",
249
+ description="Resilient gRPC channel options with DNS re-resolution, keepalive, and retries",
182
250
  )
183
251
 
184
252
  @field_validator("credentials")
@@ -204,6 +272,15 @@ class ClientConfig(ChannelConfig):
204
272
  raise ConfigurationError(msg)
205
273
  return v
206
274
 
275
+ @property
276
+ def grpc_options(self) -> list[tuple[str, Any]]:
277
+ """Get channel options with retry policy service config.
278
+
279
+ Returns:
280
+ Full list of gRPC channel options.
281
+ """
282
+ return [*self.channel_options, ("grpc.service_config", self.retry_policy.to_service_config_json())]
283
+
207
284
 
208
285
  class ServerConfig(ChannelConfig):
209
286
  """Base configuration for gRPC servers.
@@ -223,10 +300,18 @@ class ServerConfig(ChannelConfig):
223
300
  credentials: ServerCredentials | None = Field(None, description="Server credentials for secure mode")
224
301
  server_options: list[tuple[str, Any]] = Field(
225
302
  default_factory=lambda: [
226
- ("grpc.max_receive_message_length", 100 * 1024 * 1024), # 100MB
227
- ("grpc.max_send_message_length", 100 * 1024 * 1024), # 100MB
303
+ ("grpc.max_receive_message_length", 100 * 1024 * 1024),
304
+ ("grpc.max_send_message_length", 100 * 1024 * 1024),
305
+ # === Keepalive Permission (Required for Client Keepalive) ===
306
+ # Allow clients to send keepalive pings without active RPCs
307
+ # Without this, server rejects client keepalives with GOAWAY
308
+ ("grpc.keepalive_permit_without_calls", True),
309
+ # Minimum interval server allows between client pings (10 seconds)
310
+ # Prevents "too_many_pings" GOAWAY errors
311
+ # Must match or be less than client's http2.min_time_between_pings_ms
312
+ ("grpc.http2.min_ping_interval_without_data_ms", 10000),
228
313
  ],
229
- description="Additional server options",
314
+ description="gRPC server options with keepalive support",
230
315
  )
231
316
  enable_reflection: bool = Field(default=True, description="Enable reflection for the server")
232
317
  enable_health_check: bool = Field(default=True, description="Enable health check service")
@@ -140,10 +140,15 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
140
140
 
141
141
  clean_fields[name] = (current_annotation, current_field_info)
142
142
 
143
+ root_extra = cls.model_config.get("json_schema_extra", {})
144
+
143
145
  m = create_model(
144
146
  f"{cls.__name__}",
145
147
  __base__=SetupModel,
146
- __config__=ConfigDict(arbitrary_types_allowed=True),
148
+ __config__=ConfigDict(
149
+ arbitrary_types_allowed=True,
150
+ json_schema_extra=copy.deepcopy(root_extra) if isinstance(root_extra, dict) else root_extra,
151
+ ),
147
152
  **clean_fields,
148
153
  )
149
154
 
@@ -280,10 +285,15 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
280
285
  if not has_changes:
281
286
  return model_cls
282
287
 
288
+ root_extra = cls.model_config.get("json_schema_extra", {})
289
+
283
290
  return create_model(
284
291
  model_cls.__name__,
285
292
  __base__=BaseModel,
286
- __config__=ConfigDict(arbitrary_types_allowed=True),
293
+ __config__=ConfigDict(
294
+ arbitrary_types_allowed=True,
295
+ json_schema_extra=copy.deepcopy(root_extra) if isinstance(root_extra, dict) else root_extra,
296
+ ),
287
297
  **clean_fields,
288
298
  )
289
299
 
@@ -437,7 +447,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
437
447
  logger.info("Tool cache built: %d entries", len(cache.entries))
438
448
  return cache
439
449
 
440
- def _build_tool_cache_recursive(self, model_instance: BaseModel, cache: ToolCache) -> None: # noqa: C901, PLR0912
450
+ def _build_tool_cache_recursive(self, model_instance: BaseModel, cache: ToolCache) -> None: # noqa: C901
441
451
  """Recursively build tool cache and populate companion fields.
442
452
 
443
453
  Args:
@@ -447,13 +457,16 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
447
457
  for field_name, field_value in model_instance.__dict__.items():
448
458
  if field_value is None:
449
459
  continue
450
-
451
- if isinstance(field_value, ToolReference) and field_value.module_info:
460
+ if isinstance(field_value, ToolReference):
452
461
  cache_field_name = f"{field_name}_cache"
453
- if cache_field_name in type(model_instance).model_fields:
454
- setattr(model_instance, cache_field_name, field_value.module_info)
455
- cache.add(field_value.module_info.module_id, field_value.module_info)
456
- logger.debug("Added tool to cache: %s", field_value.module_info.module_id)
462
+
463
+ cached_info = getattr(model_instance, cache_field_name, None)
464
+ module_info = field_value.module_info or cached_info
465
+ if module_info:
466
+ if not cached_info:
467
+ setattr(model_instance, cache_field_name, module_info)
468
+ cache.add(module_info.module_id, module_info)
469
+ logger.debug("Added tool to cache: %s", module_info.module_id)
457
470
  elif isinstance(field_value, BaseModel):
458
471
  self._build_tool_cache_recursive(field_value, cache)
459
472
  elif isinstance(field_value, list):
@@ -473,12 +486,5 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
473
486
  self._build_tool_cache_recursive(item, cache)
474
487
 
475
488
  # Update companion field with resolved infos
476
- if resolved_infos and cache_field_name in type(model_instance).model_fields:
489
+ if resolved_infos:
477
490
  setattr(model_instance, cache_field_name, resolved_infos)
478
- elif isinstance(field_value, dict):
479
- for item in field_value.values():
480
- if isinstance(item, ToolReference) and item.module_info:
481
- cache.add(item.module_info.module_id, item.module_info)
482
- logger.debug("Added tool to cache: %s", item.module_info.module_id)
483
- elif isinstance(item, BaseModel):
484
- self._build_tool_cache_recursive(item, cache)
@@ -20,9 +20,9 @@ class ToolReferenceConfig(BaseModel):
20
20
  """Tool selection configuration. The module_id serves as both identifier and cache key."""
21
21
 
22
22
  mode: ToolSelectionMode = Field(default=ToolSelectionMode.FIXED)
23
- module_id: str | None = Field(default=None)
24
- tag: str | None = Field(default=None)
25
- organization_id: str | None = Field(default=None)
23
+ module_id: str = Field(default="")
24
+ tag: str = Field(default="")
25
+ organization_id: str = Field(default="")
26
26
 
27
27
  @model_validator(mode="after")
28
28
  def validate_config(self) -> "ToolReferenceConfig":
@@ -50,7 +50,7 @@ class ToolReference(BaseModel):
50
50
  _cached_info: ModuleInfo | None = PrivateAttr(default=None)
51
51
 
52
52
  @property
53
- def slug(self) -> str | None:
53
+ def slug(self) -> str:
54
54
  """Cache key (same as module_id).
55
55
 
56
56
  Returns:
@@ -59,11 +59,11 @@ class ToolReference(BaseModel):
59
59
  return self.config.module_id
60
60
 
61
61
  @property
62
- def module_id(self) -> str | None:
62
+ def module_id(self) -> str:
63
63
  """Module identifier.
64
64
 
65
65
  Returns:
66
- Module ID or None if not set.
66
+ Module ID or empty string if not set.
67
67
  """
68
68
  return self.config.module_id
69
69
 
@@ -22,8 +22,8 @@ from digitalkin.models.module.utility import EndOfStreamOutput, ModuleStartInfoO
22
22
  from digitalkin.models.services.storage import BaseRole
23
23
  from digitalkin.modules.trigger_handler import TriggerHandler
24
24
  from digitalkin.services.services_config import ServicesConfig, ServicesStrategy
25
- from digitalkin.utils.llm_ready_schema import llm_ready_schema
26
25
  from digitalkin.utils.package_discover import ModuleDiscoverer
26
+ from digitalkin.utils.schema_splitter import SchemaSplitter
27
27
 
28
28
 
29
29
  class BaseModule( # noqa: PLR0904
@@ -137,7 +137,8 @@ class BaseModule( # noqa: PLR0904
137
137
  """
138
138
  if cls.secret_format is not None:
139
139
  if llm_format:
140
- return json.dumps(llm_ready_schema(cls.secret_format), indent=2)
140
+ result_json, result_ui = SchemaSplitter.split(cls.secret_format.model_json_schema())
141
+ return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
141
142
  return json.dumps(cls.secret_format.model_json_schema(), indent=2)
142
143
  msg = f"{cls.__name__}' class does not define a 'secret_format'."
143
144
  raise NotImplementedError(msg)
@@ -163,7 +164,8 @@ class BaseModule( # noqa: PLR0904
163
164
  extended_model = UtilitySchemaExtender.create_extended_input_model(cls.input_format)
164
165
 
165
166
  if llm_format:
166
- return json.dumps(llm_ready_schema(extended_model), indent=2)
167
+ result_json, result_ui = SchemaSplitter.split(extended_model.model_json_schema())
168
+ return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
167
169
  return json.dumps(extended_model.model_json_schema(), indent=2)
168
170
 
169
171
  @classmethod
@@ -187,7 +189,8 @@ class BaseModule( # noqa: PLR0904
187
189
  extended_model = UtilitySchemaExtender.create_extended_output_model(cls.output_format)
188
190
 
189
191
  if llm_format:
190
- return json.dumps(llm_ready_schema(extended_model), indent=2)
192
+ result_json, result_ui = SchemaSplitter.split(extended_model.model_json_schema())
193
+ return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
191
194
  return json.dumps(extended_model.model_json_schema(), indent=2)
192
195
 
193
196
  @classmethod
@@ -215,7 +218,8 @@ class BaseModule( # noqa: PLR0904
215
218
  if cls.setup_format is not None:
216
219
  setup_format = await cls.setup_format.get_clean_model(config_fields=True, hidden_fields=False, force=True)
217
220
  if llm_format:
218
- return json.dumps(llm_ready_schema(setup_format), indent=2)
221
+ result_json, result_ui = SchemaSplitter.split(setup_format.model_json_schema())
222
+ return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
219
223
  return json.dumps(setup_format.model_json_schema(), indent=2)
220
224
  msg = "'%s' class does not define an 'config_setup_format'."
221
225
  raise NotImplementedError(msg)
@@ -244,7 +248,8 @@ class BaseModule( # noqa: PLR0904
244
248
  if cls.setup_format is not None:
245
249
  setup_format = await cls.setup_format.get_clean_model(config_fields=False, hidden_fields=True, force=True)
246
250
  if llm_format:
247
- return json.dumps(llm_ready_schema(setup_format), indent=2)
251
+ result_json, result_ui = SchemaSplitter.split(setup_format.model_json_schema())
252
+ return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
248
253
  return json.dumps(setup_format.model_json_schema(), indent=2)
249
254
  msg = "'%s' class does not define an 'setup_format'."
250
255
  raise NotImplementedError(msg)
@@ -0,0 +1,207 @@
1
+ """Schema splitter for react-jsonschema-form."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class SchemaSplitter:
7
+ """Splits a combined JSON schema into jsonschema and uischema for react-jsonschema-form."""
8
+
9
+ @classmethod
10
+ def split(cls, combined_schema: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
11
+ """Split schema into (jsonschema, uischema).
12
+
13
+ Args:
14
+ combined_schema: Combined JSON schema with ui:* properties.
15
+
16
+ Returns:
17
+ Tuple of (jsonschema, uischema).
18
+ """
19
+ defs_ui: dict[str, dict[str, Any]] = {}
20
+ if "$defs" in combined_schema:
21
+ for def_name, def_value in combined_schema["$defs"].items():
22
+ if isinstance(def_value, dict):
23
+ defs_ui[def_name] = {}
24
+ cls._extract_ui_properties(def_value, defs_ui[def_name])
25
+
26
+ json_schema: dict[str, Any] = {}
27
+ ui_schema: dict[str, Any] = {}
28
+ cls._process_object(combined_schema, json_schema, ui_schema, defs_ui)
29
+ return json_schema, ui_schema
30
+
31
+ @classmethod
32
+ def _extract_ui_properties(cls, source: dict[str, Any], ui_target: dict[str, Any]) -> None: # noqa: C901
33
+ """Extract ui:* properties from source into ui_target recursively.
34
+
35
+ Args:
36
+ source: Source dict to extract from.
37
+ ui_target: Target dict for ui properties.
38
+ """
39
+ for key, value in source.items():
40
+ if key.startswith("ui:"):
41
+ ui_target[key] = value
42
+ elif key == "properties" and isinstance(value, dict):
43
+ for prop_name, prop_value in value.items():
44
+ if isinstance(prop_value, dict):
45
+ prop_ui: dict[str, Any] = {}
46
+ cls._extract_ui_properties(prop_value, prop_ui)
47
+ if prop_ui:
48
+ ui_target[prop_name] = prop_ui
49
+ elif key == "items" and isinstance(value, dict):
50
+ items_ui: dict[str, Any] = {}
51
+ cls._extract_ui_properties(value, items_ui)
52
+ if items_ui:
53
+ ui_target["items"] = items_ui
54
+ elif key == "allOf" and isinstance(value, list):
55
+ for item in value:
56
+ if isinstance(item, dict):
57
+ cls._extract_ui_properties(item, ui_target)
58
+
59
+ @classmethod
60
+ def _process_object( # noqa: C901, PLR0912
61
+ cls,
62
+ source: dict[str, Any],
63
+ json_target: dict[str, Any],
64
+ ui_target: dict[str, Any],
65
+ defs_ui: dict[str, dict[str, Any]],
66
+ ) -> None:
67
+ """Process an object, splitting json and ui properties.
68
+
69
+ Args:
70
+ source: Source object to process.
71
+ json_target: Target dict for json schema.
72
+ ui_target: Target dict for ui schema.
73
+ defs_ui: Pre-extracted UI properties from $defs.
74
+ """
75
+ for key, value in source.items():
76
+ if key.startswith("ui:"):
77
+ ui_target[key] = value
78
+ elif key == "properties" and isinstance(value, dict):
79
+ json_target["properties"] = {}
80
+ for prop_name, prop_value in value.items():
81
+ if isinstance(prop_value, dict):
82
+ json_target["properties"][prop_name] = {}
83
+ prop_ui: dict[str, Any] = {}
84
+ cls._process_property(prop_value, json_target["properties"][prop_name], prop_ui, defs_ui)
85
+ if prop_ui:
86
+ ui_target[prop_name] = prop_ui
87
+ else:
88
+ json_target["properties"][prop_name] = prop_value
89
+ elif key == "$defs" and isinstance(value, dict):
90
+ json_target["$defs"] = {}
91
+ for def_name, def_value in value.items():
92
+ if isinstance(def_value, dict):
93
+ json_target["$defs"][def_name] = {}
94
+ cls._strip_ui_properties(def_value, json_target["$defs"][def_name])
95
+ else:
96
+ json_target["$defs"][def_name] = def_value
97
+ elif key == "items" and isinstance(value, dict):
98
+ json_target["items"] = {}
99
+ items_ui: dict[str, Any] = {}
100
+ cls._process_property(value, json_target["items"], items_ui, defs_ui)
101
+ if items_ui:
102
+ ui_target["items"] = items_ui
103
+ elif key == "allOf" and isinstance(value, list):
104
+ json_target["allOf"] = []
105
+ for item in value:
106
+ if isinstance(item, dict):
107
+ item_json: dict[str, Any] = {}
108
+ cls._strip_ui_properties(item, item_json)
109
+ json_target["allOf"].append(item_json)
110
+ else:
111
+ json_target["allOf"].append(item)
112
+ elif key in {"if", "then", "else"} and isinstance(value, dict):
113
+ json_target[key] = {}
114
+ cls._strip_ui_properties(value, json_target[key])
115
+ else:
116
+ json_target[key] = value
117
+
118
+ @classmethod
119
+ def _process_property( # noqa: C901, PLR0912
120
+ cls,
121
+ source: dict[str, Any],
122
+ json_target: dict[str, Any],
123
+ ui_target: dict[str, Any],
124
+ defs_ui: dict[str, dict[str, Any]],
125
+ ) -> None:
126
+ """Process a property, resolving $ref for UI properties.
127
+
128
+ Args:
129
+ source: Source property dict.
130
+ json_target: Target dict for json schema.
131
+ ui_target: Target dict for ui schema.
132
+ defs_ui: Pre-extracted UI properties from $defs.
133
+ """
134
+ if "$ref" in source:
135
+ ref_path = source["$ref"]
136
+ if ref_path.startswith("#/$defs/"):
137
+ def_name = ref_path[8:]
138
+ if def_name in defs_ui:
139
+ ui_target.update(defs_ui[def_name])
140
+
141
+ for key, value in source.items():
142
+ if key.startswith("ui:"):
143
+ ui_target[key] = value
144
+ elif key == "properties" and isinstance(value, dict):
145
+ json_target["properties"] = {}
146
+ for prop_name, prop_value in value.items():
147
+ if isinstance(prop_value, dict):
148
+ json_target["properties"][prop_name] = {}
149
+ prop_ui: dict[str, Any] = {}
150
+ cls._process_property(prop_value, json_target["properties"][prop_name], prop_ui, defs_ui)
151
+ if prop_ui:
152
+ ui_target[prop_name] = prop_ui
153
+ else:
154
+ json_target["properties"][prop_name] = prop_value
155
+ elif key == "items" and isinstance(value, dict):
156
+ json_target["items"] = {}
157
+ items_ui: dict[str, Any] = {}
158
+ cls._process_property(value, json_target["items"], items_ui, defs_ui)
159
+ if items_ui:
160
+ ui_target["items"] = items_ui
161
+ else:
162
+ json_target[key] = value
163
+
164
+ @classmethod
165
+ def _strip_ui_properties(cls, source: dict[str, Any], json_target: dict[str, Any]) -> None: # noqa: C901, PLR0912
166
+ """Copy source to json_target, stripping ui:* properties.
167
+
168
+ Args:
169
+ source: Source dict.
170
+ json_target: Target dict without ui:* properties.
171
+ """
172
+ for key, value in source.items():
173
+ if key.startswith("ui:"):
174
+ continue
175
+ if key == "properties" and isinstance(value, dict):
176
+ json_target["properties"] = {}
177
+ for prop_name, prop_value in value.items():
178
+ if isinstance(prop_value, dict):
179
+ json_target["properties"][prop_name] = {}
180
+ cls._strip_ui_properties(prop_value, json_target["properties"][prop_name])
181
+ else:
182
+ json_target["properties"][prop_name] = prop_value
183
+ elif key == "$defs" and isinstance(value, dict):
184
+ json_target["$defs"] = {}
185
+ for def_name, def_value in value.items():
186
+ if isinstance(def_value, dict):
187
+ json_target["$defs"][def_name] = {}
188
+ cls._strip_ui_properties(def_value, json_target["$defs"][def_name])
189
+ else:
190
+ json_target["$defs"][def_name] = def_value
191
+ elif key == "items" and isinstance(value, dict):
192
+ json_target["items"] = {}
193
+ cls._strip_ui_properties(value, json_target["items"])
194
+ elif key == "allOf" and isinstance(value, list):
195
+ json_target["allOf"] = []
196
+ for item in value:
197
+ if isinstance(item, dict):
198
+ item_json: dict[str, Any] = {}
199
+ cls._strip_ui_properties(item, item_json)
200
+ json_target["allOf"].append(item_json)
201
+ else:
202
+ json_target["allOf"].append(item)
203
+ elif key in {"if", "then", "else"} and isinstance(value, dict):
204
+ json_target[key] = {}
205
+ cls._strip_ui_properties(value, json_target[key])
206
+ else:
207
+ json_target[key] = value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.3.2.dev8
3
+ Version: 0.3.2.dev11
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
@@ -7,7 +7,7 @@ base_server/mock/__init__.py,sha256=YZFT-F1l_TpvJYuIPX-7kTeE1CfOjhx9YmNRXVoi-jQ,
7
7
  base_server/mock/mock_pb2.py,sha256=sETakcS3PAAm4E-hTCV1jIVaQTPEAIoVVHupB8Z_k7Y,1843
8
8
  base_server/mock/mock_pb2_grpc.py,sha256=BbOT70H6q3laKgkHfOx1QdfmCS_HxCY4wCOX84YAdG4,3180
9
9
  digitalkin/__init__.py,sha256=7LLBAba0th-3SGqcpqFO-lopWdUkVLKzLZiMtB-mW3M,162
10
- digitalkin/__version__.py,sha256=6FlGtVDDDa54zfBX0BGJLxwTqRJZh2f79aYSPDGZ1mk,195
10
+ digitalkin/__version__.py,sha256=eMTzoZPBXP9ODmQLjGnFO2VyPG4pFrTsF4GQwtUyQzI,196
11
11
  digitalkin/logger.py,sha256=8ze_tjt2G6mDTuQcsf7-UTXWP3UHZ7LZVSs_iqF4rX4,4685
12
12
  digitalkin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  digitalkin/core/__init__.py,sha256=FJRcJ-B1Viyn-38L8XpOpZ8KOnf1I7PCDOAmKXLQhqc,71
@@ -31,7 +31,7 @@ digitalkin/grpc_servers/module_server.py,sha256=Ec3izzV2YpdN8rGs_cX-iVulQ00FkLR5
31
31
  digitalkin/grpc_servers/module_servicer.py,sha256=7GQOyAPYMxHVaJGplgDNiVoKr1oaAIL-zdZpyDpznTA,20530
32
32
  digitalkin/grpc_servers/utils/__init__.py,sha256=ZnAIb_F8z4NhtPypqkdmzgRSzolKnJTk3oZx5GfWH5Y,38
33
33
  digitalkin/grpc_servers/utils/exceptions.py,sha256=LtaDtlqXCeT6iqApogs4pbtezotOVeg4fhnFzGBvFsY,692
34
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py,sha256=ElGvp6evY5q-EBmDVyQZaDJktfShtMsptMmq16jfgxA,3285
34
+ digitalkin/grpc_servers/utils/grpc_client_wrapper.py,sha256=nGG8QdKnBH0UG9qbKrlPwIvcvPgW3osw7O3cImxisPE,3279
35
35
  digitalkin/grpc_servers/utils/grpc_error_handler.py,sha256=0wPEU4713_ZlgIilaeXJV2bi90tHwYO1myDrSLeenKk,1848
36
36
  digitalkin/grpc_servers/utils/utility_schema_extender.py,sha256=UCJR5YAKA_Wg5Q9feInf4AZW6AVtSQZYRZuMsaEgZVo,3775
37
37
  digitalkin/mixins/__init__.py,sha256=d6ljaoyJZJT9XxOrXZG5FVNvbLURb3_CZrkp4GPZWYM,590
@@ -48,23 +48,23 @@ digitalkin/models/core/__init__.py,sha256=jOMDmPX0uSfGA9zUi0u_kOvYJ46VdIssoIhVYv
48
48
  digitalkin/models/core/job_manager_models.py,sha256=wvf2dzRzAu0-zzzAXQe6XTC36cNA10sXRLt2p_TFqjk,1003
49
49
  digitalkin/models/core/task_monitor.py,sha256=CW-jydSgXMV464W0pqfar0HpgqlSxqdujmC-f8p9EQc,2639
50
50
  digitalkin/models/grpc_servers/__init__.py,sha256=0tA71nPSXgRrh9DoLvx-TSwZXdYIRUEItoadpTL1cTo,42
51
- digitalkin/models/grpc_servers/models.py,sha256=unV1Wo0u3Efm7ddgYyYZZYUC_W6F0S5BQYH3xsOmXjw,8965
51
+ digitalkin/models/grpc_servers/models.py,sha256=gRX94eL71a5mLIie-lCOwE7a0As_AuGduxPPzTHbAe4,13797
52
52
  digitalkin/models/grpc_servers/types.py,sha256=rQ78s4nAet2jy-NIDj_PUWriT0kuGHr_w6ELjmjgBao,539
53
53
  digitalkin/models/module/__init__.py,sha256=N55wan3rAUVPEGLIDjXoFM_-DYY_zxvbQOZHzNDfwoY,751
54
54
  digitalkin/models/module/base_types.py,sha256=oIylVNqo0idTFj4dRgCt7P19daNZ-AlvgCPpL9TJvto,1850
55
55
  digitalkin/models/module/module.py,sha256=k0W8vfJJFth8XdDzkHm32SyTuSf3h2qF0hSrxAfGF1s,956
56
56
  digitalkin/models/module/module_context.py,sha256=qpjyMYgTCyQS0lW2RhTKZsoQKwFpv6l08FIM0sqC6DQ,10326
57
57
  digitalkin/models/module/module_types.py,sha256=C9azCNBk76xMa-Mww8_6AiwQR8MLAsEyUOvBYxytovI,739
58
- digitalkin/models/module/setup_types.py,sha256=0P-yG9wqhqgFDMQBl6PvcGAvhqYaDcF8SxvEFmrrkFY,18352
58
+ digitalkin/models/module/setup_types.py,sha256=XMKyDm-7n_Za9tDBv1AkgXUowlUj46SMxWANa_yg_LU,18311
59
59
  digitalkin/models/module/tool_cache.py,sha256=RP3JwASV8dFUZEKudyALbu0_tq81lstuEc4QHMQpmvM,2073
60
- digitalkin/models/module/tool_reference.py,sha256=1AyNK-8McsSTEb62aaSou5AAXkKUOJqrFtJ5YWrFbmw,3512
60
+ digitalkin/models/module/tool_reference.py,sha256=6GhLQC4pqCV3b7Nmm0mr0TZC13RDHIUOm1J-pIdWsmo,3479
61
61
  digitalkin/models/module/utility.py,sha256=gnbYfWpXGbomUI0fWf7T-Qm_VvT-LXDv1OuA9zObwVg,5589
62
62
  digitalkin/models/services/__init__.py,sha256=jhfVw6egq0OcHmos_fypH9XFehbHTBw09wluVFVFEyw,226
63
63
  digitalkin/models/services/cost.py,sha256=9PXvd5RrIk9vCrRjcUGQ9ZyAokEbwLg4s0RfnE-aLP4,1616
64
64
  digitalkin/models/services/registry.py,sha256=hz_r03-K633XHu2fOb5HWsE59EPFusBBipy0Gz6OBvI,705
65
65
  digitalkin/models/services/storage.py,sha256=wp7F-AvTsU46ujGPcguqM5kUKRZx4399D4EGAAJt2zs,1143
66
66
  digitalkin/modules/__init__.py,sha256=vTQk8DWopxQSJ17BjE5dNhq247Rou55iQLJdBxoPUmo,296
67
- digitalkin/modules/_base_module.py,sha256=KPYubZDPYIC_mWR12Vk9GMOb1mUTUnWPjbQTRBNI3XQ,21872
67
+ digitalkin/modules/_base_module.py,sha256=4-fS376f9ti3xSgM946yRccHcEGttdo2QYKaxdqk1kY,22451
68
68
  digitalkin/modules/archetype_module.py,sha256=XC9tl1Yr6QlbPn_x0eov6UUZwQgwW--BYPPMYVJH_NU,505
69
69
  digitalkin/modules/tool_module.py,sha256=GBis7bKCkvWFCYLRvaS9oZVmLBBve1w8BhVnKOU2sCc,506
70
70
  digitalkin/modules/trigger_handler.py,sha256=qPNMi-8NHqscOxciHeaXtpwjXApT3YzjMF23zQAjaZY,1770
@@ -121,7 +121,8 @@ digitalkin/utils/development_mode_action.py,sha256=2hznh0ajW_4ZTysfoc0Y49161f_PQ
121
121
  digitalkin/utils/dynamic_schema.py,sha256=5-B3dBGlCYYv6uRJkgudtc0ZpBOTYxl0yKedDGsteZQ,15184
122
122
  digitalkin/utils/llm_ready_schema.py,sha256=JjMug_lrQllqFoanaC091VgOqwAd-_YzcpqFlS7p778,2375
123
123
  digitalkin/utils/package_discover.py,sha256=sa6Zp5Kape1Zr4iYiNrnZxiHDnqM06ODk6yfWHom53w,13465
124
- digitalkin-0.3.2.dev8.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
124
+ digitalkin/utils/schema_splitter.py,sha256=KMvYRHDHlwdhh_c6FJxkWvLStZo9Kbj-jd3pIGPZfxk,9317
125
+ digitalkin-0.3.2.dev11.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
125
126
  modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
126
127
  modules/archetype_with_tools_module.py,sha256=PXTS6IXmC_OjxTmVrL_pYVI0MKwXjD5I1UJO_2xa10Q,7632
127
128
  modules/cpu_intensive_module.py,sha256=GZlirQDZdYuXrI46sv1q4RNAHZjL4EptHVQTvgK9zz8,8363
@@ -136,7 +137,7 @@ monitoring/digitalkin_observability/prometheus.py,sha256=gDmM9ySaVwPAe7Yg84pLxmE
136
137
  monitoring/tests/test_metrics.py,sha256=ugnYfAwqBPO6zA8z4afKTlyBWECTivacYSN-URQCn2E,5856
137
138
  services/filesystem_module.py,sha256=U4dgqtuDadaXz8PJ1d_uQ_1EPncBqudAQCLUICF9yL4,7421
138
139
  services/storage_module.py,sha256=Wz2MzLvqs2D_bnBBgtnujYcAKK2V2KFMk8K21RoepSE,6972
139
- digitalkin-0.3.2.dev8.dist-info/METADATA,sha256=YiEvn9TVu3pZVxk3U2bdNqvJgjpHdN48HSJe25nSr7M,29724
140
- digitalkin-0.3.2.dev8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
141
- digitalkin-0.3.2.dev8.dist-info/top_level.txt,sha256=AYVIesKrO0jnedQ-Muog9JBehG81WeTCNeOFoJgwsgE,51
142
- digitalkin-0.3.2.dev8.dist-info/RECORD,,
140
+ digitalkin-0.3.2.dev11.dist-info/METADATA,sha256=s-vXymah7gLTIqKfX_ZpR15FYj9R47Ri74ipeiW2GwA,29725
141
+ digitalkin-0.3.2.dev11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
142
+ digitalkin-0.3.2.dev11.dist-info/top_level.txt,sha256=AYVIesKrO0jnedQ-Muog9JBehG81WeTCNeOFoJgwsgE,51
143
+ digitalkin-0.3.2.dev11.dist-info/RECORD,,