rocket-welder-sdk 1.1.25__py3-none-any.whl → 1.1.27__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.
@@ -0,0 +1,30 @@
1
+ """External Controls module for RocketWelder SDK."""
2
+
3
+ from rocket_welder_sdk.ui.value_types import ControlType
4
+
5
+ from .contracts import (
6
+ ArrowDirection,
7
+ # Events (UI → Container)
8
+ ButtonDown,
9
+ ButtonUp,
10
+ ChangeControls,
11
+ # Commands (Container → UI)
12
+ DefineControl,
13
+ DeleteControl, # Legacy alias - deprecated
14
+ DeleteControls,
15
+ KeyDown,
16
+ KeyUp,
17
+ )
18
+
19
+ __all__ = [
20
+ "ArrowDirection",
21
+ "ButtonDown",
22
+ "ButtonUp",
23
+ "ChangeControls",
24
+ "ControlType", # Now using the single ControlType from ui.value_types
25
+ "DefineControl",
26
+ "DeleteControl", # Legacy - deprecated
27
+ "DeleteControls",
28
+ "KeyDown",
29
+ "KeyUp",
30
+ ]
@@ -0,0 +1,100 @@
1
+ """External Controls contracts using Pydantic v2 for modern serialization.
2
+
3
+ This module defines the contracts for external controls communication between
4
+ containers and the RocketWelder UI via EventStore, using Pydantic v2 for
5
+ proper serialization with PascalCase support.
6
+ """
7
+
8
+ from enum import Enum
9
+ from uuid import UUID, uuid4
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer
12
+ from pydantic.alias_generators import to_pascal
13
+
14
+ from rocket_welder_sdk.ui.value_types import ControlType
15
+
16
+
17
+ class ArrowDirection(str, Enum):
18
+ """Arrow directions for arrow grid control."""
19
+
20
+ UP = "Up"
21
+ DOWN = "Down"
22
+ LEFT = "Left"
23
+ RIGHT = "Right"
24
+
25
+
26
+ class BaseContract(BaseModel):
27
+ """Base class for all contracts with PascalCase serialization."""
28
+
29
+ model_config = ConfigDict(
30
+ # Convert snake_case fields to PascalCase for serialization
31
+ alias_generator=to_pascal,
32
+ # Allow both snake_case and PascalCase when deserializing
33
+ populate_by_name=True,
34
+ # Use Enum values for serialization
35
+ use_enum_values=True,
36
+ )
37
+
38
+ id: UUID = Field(default_factory=uuid4)
39
+
40
+ @field_serializer("id")
41
+ def serialize_id(self, value: UUID) -> str:
42
+ """Serialize UUID as string for JSON compatibility."""
43
+ return str(value)
44
+
45
+
46
+ # Container → UI Commands (Stream: ExternalCommands-{SessionId})
47
+
48
+
49
+ class DefineControl(BaseContract):
50
+ """Command to define a new control in the UI."""
51
+
52
+ control_id: str
53
+ type: ControlType
54
+ properties: dict[str, str]
55
+ region_name: str
56
+
57
+
58
+ class DeleteControls(BaseContract):
59
+ """Command to delete multiple controls from the UI."""
60
+
61
+ control_ids: list[str]
62
+
63
+
64
+ # Legacy alias for backward compatibility (will be removed)
65
+ DeleteControl = DeleteControls
66
+
67
+
68
+ class ChangeControls(BaseContract):
69
+ """Command to update properties of multiple controls."""
70
+
71
+ updates: dict[str, dict[str, str]] # ControlId -> {PropertyId -> Value}
72
+
73
+
74
+ # UI → Container Events (Stream: ExternalEvents-{SessionId})
75
+
76
+
77
+ class ButtonDown(BaseContract):
78
+ """Event when a button is pressed down."""
79
+
80
+ control_id: str
81
+
82
+
83
+ class ButtonUp(BaseContract):
84
+ """Event when a button is released."""
85
+
86
+ control_id: str
87
+
88
+
89
+ class KeyDown(BaseContract):
90
+ """Event when a key is pressed down."""
91
+
92
+ control_id: str
93
+ code: str # KeyCode value like "ArrowUp", "Enter", etc.
94
+
95
+
96
+ class KeyUp(BaseContract):
97
+ """Event when a key is released."""
98
+
99
+ control_id: str
100
+ code: str # KeyCode value like "ArrowUp", "Enter", etc.
@@ -0,0 +1,105 @@
1
+ """External Controls event contracts for RocketWelder SDK (legacy - for backward compatibility)."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from uuid import UUID, uuid4
6
+
7
+ from rocket_welder_sdk.ui.value_types import ControlType
8
+
9
+
10
+ class ArrowDirection(Enum):
11
+ """Arrow directions for ArrowGrid control."""
12
+
13
+ UP = "Up"
14
+ DOWN = "Down"
15
+ LEFT = "Left"
16
+ RIGHT = "Right"
17
+
18
+
19
+ # Container → UI Commands (Stream: ExternalCommands-{SessionId})
20
+
21
+
22
+ @dataclass
23
+ class DefineControl:
24
+ """Command to define a new control in the UI."""
25
+
26
+ control_id: str
27
+ type: ControlType
28
+ properties: dict[str, str]
29
+ region_name: str
30
+ id: UUID = field(default_factory=uuid4)
31
+
32
+ def to_dict(self) -> dict[str, object]:
33
+ """Convert to dictionary for EventStore."""
34
+ return {
35
+ "Id": str(self.id),
36
+ "ControlId": self.control_id,
37
+ "Type": self.type.value,
38
+ "Properties": self.properties,
39
+ "RegionName": self.region_name,
40
+ }
41
+
42
+
43
+ @dataclass
44
+ class DeleteControl:
45
+ """Command to delete a control from the UI."""
46
+
47
+ control_id: str
48
+ id: UUID = field(default_factory=uuid4)
49
+
50
+ def to_dict(self) -> dict[str, str]:
51
+ """Convert to dictionary for EventStore."""
52
+ return {"Id": str(self.id), "ControlId": self.control_id}
53
+
54
+
55
+ @dataclass
56
+ class ChangeControls:
57
+ """Command to update properties of multiple controls."""
58
+
59
+ updates: dict[str, dict[str, str]] # ControlId -> { PropertyId -> Value }
60
+ id: UUID = field(default_factory=uuid4)
61
+
62
+ def to_dict(self) -> dict[str, object]:
63
+ """Convert to dictionary for EventStore."""
64
+ return {"Id": str(self.id), "Updates": self.updates}
65
+
66
+
67
+ # UI → Container Events (Stream: ExternalEvents-{SessionId})
68
+
69
+
70
+ @dataclass
71
+ class ButtonDown:
72
+ """Event when button is pressed."""
73
+
74
+ control_id: str
75
+ id: UUID = field(default_factory=uuid4)
76
+
77
+ def to_dict(self) -> dict[str, str]:
78
+ """Convert to dictionary for EventStore."""
79
+ return {"Id": str(self.id), "ControlId": self.control_id}
80
+
81
+ @classmethod
82
+ def from_dict(cls, data: dict[str, object]) -> "ButtonDown":
83
+ """Create from EventStore data."""
84
+ return cls(
85
+ control_id=str(data["ControlId"]), id=UUID(str(data["Id"])) if "Id" in data else uuid4()
86
+ )
87
+
88
+
89
+ @dataclass
90
+ class ButtonUp:
91
+ """Event when button is released."""
92
+
93
+ control_id: str
94
+ id: UUID = field(default_factory=uuid4)
95
+
96
+ def to_dict(self) -> dict[str, str]:
97
+ """Convert to dictionary for EventStore."""
98
+ return {"Id": str(self.id), "ControlId": self.control_id}
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict[str, object]) -> "ButtonUp":
102
+ """Create from EventStore data."""
103
+ return cls(
104
+ control_id=str(data["ControlId"]), id=UUID(str(data["Id"])) if "Id" in data else uuid4()
105
+ )
@@ -0,0 +1,48 @@
1
+ """UI module for RocketWelder SDK."""
2
+
3
+ from rocket_welder_sdk.external_controls.contracts import ArrowDirection
4
+
5
+ from .controls import (
6
+ ArrowGridControl,
7
+ ControlBase,
8
+ IconButtonControl,
9
+ LabelControl,
10
+ )
11
+ from .icons import Custom, Icons, Material
12
+ from .ui_events_projection import UiEventsProjection
13
+ from .ui_service import (
14
+ ItemsControl,
15
+ UiControlFactory,
16
+ UiService,
17
+ )
18
+ from .value_types import (
19
+ Color,
20
+ ControlType,
21
+ RegionName,
22
+ Size,
23
+ Typography,
24
+ )
25
+
26
+ __all__ = [
27
+ "ArrowDirection",
28
+ "ArrowGridControl",
29
+ "Color",
30
+ # Controls
31
+ "ControlBase",
32
+ # Enums
33
+ "ControlType",
34
+ "Custom",
35
+ "IconButtonControl",
36
+ # Icons
37
+ "Icons",
38
+ "ItemsControl",
39
+ "LabelControl",
40
+ "Material",
41
+ "RegionName",
42
+ "Size",
43
+ "Typography",
44
+ "UiControlFactory",
45
+ "UiEventsProjection",
46
+ # Services
47
+ "UiService",
48
+ ]
@@ -0,0 +1,362 @@
1
+ """UI Control base classes and implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar
7
+
8
+ from rocket_welder_sdk.external_controls.contracts import (
9
+ ArrowDirection,
10
+ ButtonDown,
11
+ ButtonUp,
12
+ KeyDown,
13
+ KeyUp,
14
+ )
15
+
16
+ from .value_types import Color, ControlType, Size, Typography
17
+
18
+ if TYPE_CHECKING:
19
+ from .ui_service import UiService
20
+
21
+
22
+ class ControlBase(ABC):
23
+ """Base class for all UI controls."""
24
+
25
+ def __init__(
26
+ self,
27
+ control_id: str,
28
+ control_type: ControlType,
29
+ ui_service: UiService,
30
+ properties: dict[str, str] | None = None,
31
+ ) -> None:
32
+ """
33
+ Initialize base control.
34
+
35
+ Args:
36
+ control_id: Unique identifier for the control
37
+ control_type: Type of the control
38
+ ui_service: Reference to parent UiService
39
+ properties: Initial properties
40
+ """
41
+ self.id: str = control_id
42
+ self.control_type: ControlType = control_type
43
+ self._ui_service: UiService = ui_service
44
+ self._properties: dict[str, str] = properties or {}
45
+ self._changed: dict[str, str] = {}
46
+ self._is_disposed: bool = False
47
+
48
+ @property
49
+ def is_dirty(self) -> bool:
50
+ """Check if control has uncommitted changes."""
51
+ return bool(self._changed)
52
+
53
+ @property
54
+ def changed(self) -> dict[str, str]:
55
+ """Get pending changes."""
56
+ return self._changed.copy()
57
+
58
+ @property
59
+ def properties(self) -> dict[str, str]:
60
+ """Get current properties including changes."""
61
+ props = self._properties.copy()
62
+ props.update(self._changed)
63
+ return props
64
+
65
+ def set_property(self, name: str, value: Any) -> None:
66
+ """
67
+ Set a property value.
68
+
69
+ Args:
70
+ name: Property name
71
+ value: Property value (will be converted to string)
72
+ """
73
+ str_value = str(value) if value is not None else ""
74
+ if self._properties.get(name) != str_value:
75
+ self._changed[name] = str_value
76
+
77
+ def commit_changes(self) -> None:
78
+ """Commit pending changes to properties."""
79
+ self._properties.update(self._changed)
80
+ self._changed.clear()
81
+
82
+ @abstractmethod
83
+ def handle_event(self, event: Any) -> None:
84
+ """
85
+ Handle an event for this control.
86
+
87
+ Args:
88
+ event: Event to handle
89
+ """
90
+ pass
91
+
92
+ def dispose(self) -> None:
93
+ """Dispose of the control."""
94
+ if not self._is_disposed:
95
+ self._is_disposed = True
96
+ self._ui_service.schedule_delete(self.id)
97
+
98
+
99
+ class IconButtonControl(ControlBase):
100
+ """Icon button control with click events."""
101
+
102
+ def __init__(
103
+ self,
104
+ control_id: str,
105
+ ui_service: UiService,
106
+ icon: str,
107
+ properties: dict[str, str] | None = None,
108
+ ) -> None:
109
+ """
110
+ Initialize icon button control.
111
+
112
+ Args:
113
+ control_id: Unique identifier
114
+ ui_service: Parent UiService
115
+ icon: SVG path for the icon
116
+ properties: Additional properties
117
+ """
118
+ props = properties or {}
119
+ props["Icon"] = icon
120
+ super().__init__(control_id, ControlType.ICON_BUTTON, ui_service, props)
121
+
122
+ # Event handlers
123
+ self.on_button_down: Callable[[IconButtonControl], None] | None = None
124
+ self.on_button_up: Callable[[IconButtonControl], None] | None = None
125
+
126
+ @property
127
+ def icon(self) -> str:
128
+ """Get icon SVG path."""
129
+ return self.properties.get("Icon", "")
130
+
131
+ @icon.setter
132
+ def icon(self, value: str) -> None:
133
+ """Set icon SVG path."""
134
+ self.set_property("Icon", value)
135
+
136
+ @property
137
+ def text(self) -> str | None:
138
+ """Get button text."""
139
+ return self.properties.get("Text")
140
+
141
+ @text.setter
142
+ def text(self, value: str | None) -> None:
143
+ """Set button text."""
144
+ if value is not None:
145
+ self.set_property("Text", value)
146
+
147
+ @property
148
+ def color(self) -> Color:
149
+ """Get button color."""
150
+ color_str = self.properties.get("Color", Color.PRIMARY.value)
151
+ try:
152
+ return Color(color_str)
153
+ except ValueError:
154
+ return Color.PRIMARY
155
+
156
+ @color.setter
157
+ def color(self, value: Color | str) -> None:
158
+ """Set button color."""
159
+ if isinstance(value, Color):
160
+ self.set_property("Color", value.value)
161
+ else:
162
+ # Try to find matching enum
163
+ for color in Color:
164
+ if color.value == value:
165
+ self.set_property("Color", value)
166
+ return
167
+ raise ValueError(f"Invalid color value: {value}")
168
+
169
+ @property
170
+ def size(self) -> Size:
171
+ """Get button size."""
172
+ size_str = self.properties.get("Size", Size.MEDIUM.value)
173
+ try:
174
+ return Size(size_str)
175
+ except ValueError:
176
+ return Size.MEDIUM
177
+
178
+ @size.setter
179
+ def size(self, value: Size | str) -> None:
180
+ """Set button size."""
181
+ if isinstance(value, Size):
182
+ self.set_property("Size", value.value)
183
+ else:
184
+ # Try to find matching enum
185
+ for size in Size:
186
+ if size.value == value:
187
+ self.set_property("Size", value)
188
+ return
189
+ raise ValueError(f"Invalid size value: {value}")
190
+
191
+ def handle_event(self, event: Any) -> None:
192
+ """Handle button events."""
193
+ if isinstance(event, ButtonDown) and self.on_button_down:
194
+ self.on_button_down(self)
195
+ elif isinstance(event, ButtonUp) and self.on_button_up:
196
+ self.on_button_up(self)
197
+
198
+
199
+ class ArrowGridControl(ControlBase):
200
+ """Arrow grid control for directional input."""
201
+
202
+ # Mapping from key codes to arrow directions
203
+ KEY_TO_DIRECTION: ClassVar[dict[str, ArrowDirection]] = {
204
+ "ArrowUp": ArrowDirection.UP,
205
+ "ArrowDown": ArrowDirection.DOWN,
206
+ "ArrowLeft": ArrowDirection.LEFT,
207
+ "ArrowRight": ArrowDirection.RIGHT,
208
+ }
209
+
210
+ def __init__(
211
+ self, control_id: str, ui_service: UiService, properties: dict[str, str] | None = None
212
+ ) -> None:
213
+ """
214
+ Initialize arrow grid control.
215
+
216
+ Args:
217
+ control_id: Unique identifier
218
+ ui_service: Parent UiService
219
+ properties: Additional properties
220
+ """
221
+ super().__init__(control_id, ControlType.ARROW_GRID, ui_service, properties)
222
+
223
+ # Event handlers
224
+ self.on_arrow_down: Callable[[ArrowGridControl, ArrowDirection], None] | None = None
225
+ self.on_arrow_up: Callable[[ArrowGridControl, ArrowDirection], None] | None = None
226
+
227
+ @property
228
+ def size(self) -> Size:
229
+ """Get grid size."""
230
+ size_str = self.properties.get("Size", Size.MEDIUM.value)
231
+ try:
232
+ return Size(size_str)
233
+ except ValueError:
234
+ return Size.MEDIUM
235
+
236
+ @size.setter
237
+ def size(self, value: Size | str) -> None:
238
+ """Set grid size."""
239
+ if isinstance(value, Size):
240
+ self.set_property("Size", value.value)
241
+ else:
242
+ # Try to find matching enum
243
+ for size in Size:
244
+ if size.value == value:
245
+ self.set_property("Size", value)
246
+ return
247
+ raise ValueError(f"Invalid size value: {value}")
248
+
249
+ @property
250
+ def color(self) -> Color:
251
+ """Get grid color."""
252
+ color_str = self.properties.get("Color", Color.PRIMARY.value)
253
+ try:
254
+ return Color(color_str)
255
+ except ValueError:
256
+ return Color.PRIMARY
257
+
258
+ @color.setter
259
+ def color(self, value: Color | str) -> None:
260
+ """Set grid color."""
261
+ if isinstance(value, Color):
262
+ self.set_property("Color", value.value)
263
+ else:
264
+ # Try to find matching enum
265
+ for color in Color:
266
+ if color.value == value:
267
+ self.set_property("Color", value)
268
+ return
269
+ raise ValueError(f"Invalid color value: {value}")
270
+
271
+ def handle_event(self, event: Any) -> None:
272
+ """Handle keyboard events and translate to arrow events."""
273
+ if isinstance(event, KeyDown):
274
+ direction = self.KEY_TO_DIRECTION.get(event.code)
275
+ if direction and self.on_arrow_down:
276
+ self.on_arrow_down(self, direction)
277
+ elif isinstance(event, KeyUp):
278
+ direction = self.KEY_TO_DIRECTION.get(event.code)
279
+ if direction and self.on_arrow_up:
280
+ self.on_arrow_up(self, direction)
281
+
282
+
283
+ class LabelControl(ControlBase):
284
+ """Label control for displaying text."""
285
+
286
+ def __init__(
287
+ self,
288
+ control_id: str,
289
+ ui_service: UiService,
290
+ text: str,
291
+ properties: dict[str, str] | None = None,
292
+ ) -> None:
293
+ """
294
+ Initialize label control.
295
+
296
+ Args:
297
+ control_id: Unique identifier
298
+ ui_service: Parent UiService
299
+ text: Label text
300
+ properties: Additional properties
301
+ """
302
+ props = properties or {}
303
+ props["Text"] = text
304
+ super().__init__(control_id, ControlType.LABEL, ui_service, props)
305
+
306
+ @property
307
+ def text(self) -> str:
308
+ """Get label text."""
309
+ return self.properties.get("Text", "")
310
+
311
+ @text.setter
312
+ def text(self, value: str) -> None:
313
+ """Set label text."""
314
+ self.set_property("Text", value)
315
+
316
+ @property
317
+ def typography(self) -> Typography:
318
+ """Get label typography."""
319
+ typo_str = self.properties.get("Typography", Typography.BODY1.value)
320
+ try:
321
+ return Typography(typo_str)
322
+ except ValueError:
323
+ return Typography.BODY1
324
+
325
+ @typography.setter
326
+ def typography(self, value: Typography | str) -> None:
327
+ """Set label typography."""
328
+ if isinstance(value, Typography):
329
+ self.set_property("Typography", value.value)
330
+ else:
331
+ # Try to find matching enum
332
+ for typo in Typography:
333
+ if typo.value == value:
334
+ self.set_property("Typography", value)
335
+ return
336
+ raise ValueError(f"Invalid typography value: {value}")
337
+
338
+ @property
339
+ def color(self) -> Color:
340
+ """Get label color."""
341
+ color_str = self.properties.get("Color", Color.TEXT_PRIMARY.value)
342
+ try:
343
+ return Color(color_str)
344
+ except ValueError:
345
+ return Color.TEXT_PRIMARY
346
+
347
+ @color.setter
348
+ def color(self, value: Color | str) -> None:
349
+ """Set label color."""
350
+ if isinstance(value, Color):
351
+ self.set_property("Color", value.value)
352
+ else:
353
+ # Try to find matching enum
354
+ for color in Color:
355
+ if color.value == value:
356
+ self.set_property("Color", value)
357
+ return
358
+ raise ValueError(f"Invalid color value: {value}")
359
+
360
+ def handle_event(self, event: Any) -> None:
361
+ """Labels typically don't handle events."""
362
+ pass