rocket-welder-sdk 1.1.36.dev14__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 (40) hide show
  1. rocket_welder_sdk/__init__.py +95 -0
  2. rocket_welder_sdk/bytes_size.py +234 -0
  3. rocket_welder_sdk/connection_string.py +291 -0
  4. rocket_welder_sdk/controllers.py +831 -0
  5. rocket_welder_sdk/external_controls/__init__.py +30 -0
  6. rocket_welder_sdk/external_controls/contracts.py +100 -0
  7. rocket_welder_sdk/external_controls/contracts_old.py +105 -0
  8. rocket_welder_sdk/frame_metadata.py +138 -0
  9. rocket_welder_sdk/gst_metadata.py +411 -0
  10. rocket_welder_sdk/high_level/__init__.py +54 -0
  11. rocket_welder_sdk/high_level/client.py +235 -0
  12. rocket_welder_sdk/high_level/connection_strings.py +331 -0
  13. rocket_welder_sdk/high_level/data_context.py +169 -0
  14. rocket_welder_sdk/high_level/frame_sink_factory.py +118 -0
  15. rocket_welder_sdk/high_level/schema.py +195 -0
  16. rocket_welder_sdk/high_level/transport_protocol.py +238 -0
  17. rocket_welder_sdk/keypoints_protocol.py +642 -0
  18. rocket_welder_sdk/opencv_controller.py +278 -0
  19. rocket_welder_sdk/periodic_timer.py +303 -0
  20. rocket_welder_sdk/py.typed +2 -0
  21. rocket_welder_sdk/rocket_welder_client.py +497 -0
  22. rocket_welder_sdk/segmentation_result.py +420 -0
  23. rocket_welder_sdk/session_id.py +238 -0
  24. rocket_welder_sdk/transport/__init__.py +31 -0
  25. rocket_welder_sdk/transport/frame_sink.py +122 -0
  26. rocket_welder_sdk/transport/frame_source.py +74 -0
  27. rocket_welder_sdk/transport/nng_transport.py +197 -0
  28. rocket_welder_sdk/transport/stream_transport.py +193 -0
  29. rocket_welder_sdk/transport/tcp_transport.py +154 -0
  30. rocket_welder_sdk/transport/unix_socket_transport.py +339 -0
  31. rocket_welder_sdk/ui/__init__.py +48 -0
  32. rocket_welder_sdk/ui/controls.py +362 -0
  33. rocket_welder_sdk/ui/icons.py +21628 -0
  34. rocket_welder_sdk/ui/ui_events_projection.py +226 -0
  35. rocket_welder_sdk/ui/ui_service.py +358 -0
  36. rocket_welder_sdk/ui/value_types.py +72 -0
  37. rocket_welder_sdk-1.1.36.dev14.dist-info/METADATA +845 -0
  38. rocket_welder_sdk-1.1.36.dev14.dist-info/RECORD +40 -0
  39. rocket_welder_sdk-1.1.36.dev14.dist-info/WHEEL +5 -0
  40. rocket_welder_sdk-1.1.36.dev14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,95 @@
1
+ """
2
+ RocketWelder SDK - Enterprise-grade Python client library for video streaming services.
3
+
4
+ High-performance video streaming using shared memory (ZeroBuffer) for zero-copy operations.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+
10
+ from .bytes_size import BytesSize
11
+ from .connection_string import ConnectionMode, ConnectionString, Protocol
12
+ from .controllers import DuplexShmController, IController, OneWayShmController
13
+ from .frame_metadata import FRAME_METADATA_SIZE, FrameMetadata, GstVideoFormat
14
+ from .gst_metadata import GstCaps, GstMetadata
15
+ from .opencv_controller import OpenCvController
16
+ from .periodic_timer import PeriodicTimer, PeriodicTimerSync
17
+ from .rocket_welder_client import RocketWelderClient
18
+ from .session_id import (
19
+ # Explicit URL functions (PREFERRED - set by rocket-welder2)
20
+ ACTIONS_SINK_URL_ENV,
21
+ KEYPOINTS_SINK_URL_ENV,
22
+ SEGMENTATION_SINK_URL_ENV,
23
+ # SessionId-derived URL functions (fallback for backwards compatibility)
24
+ get_actions_url,
25
+ get_actions_url_from_env,
26
+ get_configured_nng_urls,
27
+ get_keypoints_url,
28
+ get_keypoints_url_from_env,
29
+ get_nng_urls,
30
+ get_nng_urls_from_env,
31
+ get_segmentation_url,
32
+ get_segmentation_url_from_env,
33
+ get_session_id_from_env,
34
+ has_explicit_nng_urls,
35
+ parse_session_id,
36
+ )
37
+
38
+ # Alias for backward compatibility and README examples
39
+ Client = RocketWelderClient
40
+
41
+ __version__ = "1.1.0"
42
+
43
+ # Configure library logger with NullHandler (best practice for libraries)
44
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
45
+
46
+ # Configure from environment variable and propagate to zerobuffer
47
+ _log_level = os.environ.get("ROCKET_WELDER_LOG_LEVEL")
48
+ if _log_level:
49
+ try:
50
+ # Set rocket-welder-sdk log level
51
+ logging.getLogger(__name__).setLevel(getattr(logging, _log_level.upper()))
52
+
53
+ # Propagate to zerobuffer if not already set
54
+ if not os.environ.get("ZEROBUFFER_LOG_LEVEL"):
55
+ os.environ["ZEROBUFFER_LOG_LEVEL"] = _log_level
56
+ # Also configure zerobuffer logger if already imported
57
+ zerobuffer_logger = logging.getLogger("zerobuffer")
58
+ zerobuffer_logger.setLevel(getattr(logging, _log_level.upper()))
59
+ except AttributeError:
60
+ pass # Invalid log level, ignore
61
+
62
+ __all__ = [
63
+ "ACTIONS_SINK_URL_ENV",
64
+ "FRAME_METADATA_SIZE",
65
+ "KEYPOINTS_SINK_URL_ENV",
66
+ "SEGMENTATION_SINK_URL_ENV",
67
+ "BytesSize",
68
+ "Client",
69
+ "ConnectionMode",
70
+ "ConnectionString",
71
+ "DuplexShmController",
72
+ "FrameMetadata",
73
+ "GstCaps",
74
+ "GstMetadata",
75
+ "GstVideoFormat",
76
+ "IController",
77
+ "OneWayShmController",
78
+ "OpenCvController",
79
+ "PeriodicTimer",
80
+ "PeriodicTimerSync",
81
+ "Protocol",
82
+ "RocketWelderClient",
83
+ "get_actions_url",
84
+ "get_actions_url_from_env",
85
+ "get_configured_nng_urls",
86
+ "get_keypoints_url",
87
+ "get_keypoints_url_from_env",
88
+ "get_nng_urls",
89
+ "get_nng_urls_from_env",
90
+ "get_segmentation_url",
91
+ "get_segmentation_url_from_env",
92
+ "get_session_id_from_env",
93
+ "has_explicit_nng_urls",
94
+ "parse_session_id",
95
+ ]
@@ -0,0 +1,234 @@
1
+ """
2
+ Enterprise-grade Bytes size representation with parsing support.
3
+ Matches C# Bytes struct functionality.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from dataclasses import dataclass
10
+ from typing import ClassVar
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class BytesSize:
15
+ """
16
+ Immutable representation of byte sizes with human-readable formatting.
17
+
18
+ Supports parsing from strings like "256MB", "4KB", "1.5GB" etc.
19
+ """
20
+
21
+ _value: int
22
+ _precision: int = 0
23
+
24
+ def __init__(self, value: int, precision: int = 0) -> None:
25
+ """Initialize BytesSize with value and optional precision."""
26
+ object.__setattr__(self, "_value", value)
27
+ object.__setattr__(self, "_precision", precision)
28
+
29
+ # Size multipliers
30
+ _SUFFIXES: ClassVar[dict[str, int]] = {
31
+ "B": 1,
32
+ "K": 1024,
33
+ "KB": 1024,
34
+ "M": 1024 * 1024,
35
+ "MB": 1024 * 1024,
36
+ "G": 1024 * 1024 * 1024,
37
+ "GB": 1024 * 1024 * 1024,
38
+ "T": 1024 * 1024 * 1024 * 1024,
39
+ "TB": 1024 * 1024 * 1024 * 1024,
40
+ "P": 1024 * 1024 * 1024 * 1024 * 1024,
41
+ "PB": 1024 * 1024 * 1024 * 1024 * 1024,
42
+ "E": 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
43
+ "EB": 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
44
+ }
45
+
46
+ # Pattern for parsing size strings
47
+ _PATTERN: ClassVar[re.Pattern[str]] = re.compile(r"^([\d.,]+)\s*([KMGTPE]?B?)$", re.IGNORECASE)
48
+
49
+ @property
50
+ def value(self) -> int:
51
+ """Get the raw byte value."""
52
+ return self._value
53
+
54
+ def __int__(self) -> int:
55
+ """Convert to integer."""
56
+ return self._value
57
+
58
+ def __float__(self) -> float:
59
+ """Convert to float."""
60
+ return float(self._value)
61
+
62
+ def __str__(self) -> str:
63
+ """Format as human-readable string."""
64
+ return self._format_size(self._value, self._precision)
65
+
66
+ def __repr__(self) -> str:
67
+ """Developer-friendly representation."""
68
+ return f"BytesSize({self._value}, precision={self._precision})"
69
+
70
+ def __eq__(self, other: object) -> bool:
71
+ """Equality comparison."""
72
+ if isinstance(other, BytesSize):
73
+ return self._value == other._value
74
+ if isinstance(other, (int, float)):
75
+ return self._value == other
76
+ return False
77
+
78
+ def __lt__(self, other: BytesSize | int | float) -> bool:
79
+ """Less than comparison."""
80
+ if isinstance(other, BytesSize):
81
+ return self._value < other._value
82
+ if isinstance(other, (int, float)):
83
+ return self._value < other
84
+ return NotImplemented
85
+
86
+ def __le__(self, other: BytesSize | int | float) -> bool:
87
+ """Less than or equal comparison."""
88
+ if isinstance(other, BytesSize):
89
+ return self._value <= other._value
90
+ if isinstance(other, (int, float)):
91
+ return self._value <= other
92
+ return NotImplemented
93
+
94
+ def __gt__(self, other: BytesSize | int | float) -> bool:
95
+ """Greater than comparison."""
96
+ if isinstance(other, BytesSize):
97
+ return self._value > other._value
98
+ if isinstance(other, (int, float)):
99
+ return self._value > other
100
+ return NotImplemented
101
+
102
+ def __ge__(self, other: BytesSize | int | float) -> bool:
103
+ """Greater than or equal comparison."""
104
+ if isinstance(other, BytesSize):
105
+ return self._value >= other._value
106
+ if isinstance(other, (int, float)):
107
+ return self._value >= other
108
+ return NotImplemented
109
+
110
+ def __add__(self, other: BytesSize | int) -> BytesSize:
111
+ """Add byte sizes."""
112
+ if isinstance(other, BytesSize):
113
+ return BytesSize(self._value + other._value, self._precision)
114
+ if isinstance(other, int):
115
+ return BytesSize(self._value + other, self._precision)
116
+ return NotImplemented
117
+
118
+ def __sub__(self, other: BytesSize | int) -> BytesSize:
119
+ """Subtract byte sizes."""
120
+ if isinstance(other, BytesSize):
121
+ return BytesSize(self._value - other._value, self._precision)
122
+ if isinstance(other, int):
123
+ return BytesSize(self._value - other, self._precision)
124
+ return NotImplemented
125
+
126
+ @classmethod
127
+ def parse(cls, value: str | int | float | BytesSize) -> BytesSize:
128
+ """
129
+ Parse a string, number, or BytesSize into a BytesSize object.
130
+
131
+ Args:
132
+ value: Value to parse (e.g., "256MB", 1024, "4.5GB")
133
+
134
+ Returns:
135
+ BytesSize instance
136
+
137
+ Raises:
138
+ ValueError: If the value cannot be parsed
139
+ """
140
+ if isinstance(value, BytesSize):
141
+ return value
142
+
143
+ if isinstance(value, (int, float)):
144
+ return cls(int(value))
145
+
146
+ if not isinstance(value, str):
147
+ raise ValueError(f"Cannot parse {type(value).__name__} as BytesSize")
148
+
149
+ # Clean the string
150
+ value = value.strip()
151
+ if not value:
152
+ raise ValueError("Cannot parse empty string as BytesSize")
153
+
154
+ # Try to match the pattern
155
+ match = cls._PATTERN.match(value)
156
+ if not match:
157
+ raise ValueError(f"Invalid byte size format: '{value}'")
158
+
159
+ number_str, suffix = match.groups()
160
+
161
+ # Parse the number part (handle different locales)
162
+ number_str = number_str.replace(",", "") # Remove thousands separators
163
+ try:
164
+ number = float(number_str)
165
+ except ValueError as e:
166
+ raise ValueError(f"Invalid number in byte size: '{number_str}'") from e
167
+
168
+ # Get the multiplier
169
+ suffix = suffix.upper() if suffix else "B"
170
+ multiplier = cls._SUFFIXES.get(suffix, 0)
171
+
172
+ if multiplier == 0:
173
+ raise ValueError(f"Unknown size suffix: '{suffix}'")
174
+
175
+ # Calculate the bytes
176
+ bytes_value = int(number * multiplier)
177
+ return cls(bytes_value)
178
+
179
+ @classmethod
180
+ def try_parse(cls, value: str | int | float | BytesSize) -> BytesSize | None:
181
+ """
182
+ Try to parse a value into BytesSize, returning None on failure.
183
+
184
+ Args:
185
+ value: Value to parse
186
+
187
+ Returns:
188
+ BytesSize instance or None if parsing failed
189
+ """
190
+ try:
191
+ return cls.parse(value)
192
+ except (ValueError, TypeError):
193
+ return None
194
+
195
+ @staticmethod
196
+ def _format_size(bytes_value: int, precision: int = 0) -> str:
197
+ """
198
+ Format bytes as human-readable string.
199
+
200
+ Args:
201
+ bytes_value: Number of bytes
202
+ precision: Decimal precision for formatting
203
+
204
+ Returns:
205
+ Formatted string (e.g., "256MB", "1.5GB")
206
+ """
207
+ if bytes_value == 0:
208
+ return "0B"
209
+
210
+ # Determine the appropriate unit
211
+ units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
212
+ unit_index = 0
213
+ size = float(bytes_value)
214
+
215
+ while size >= 1024 and unit_index < len(units) - 1:
216
+ size /= 1024
217
+ unit_index += 1
218
+
219
+ # Format based on precision
220
+ if precision > 0:
221
+ return f"{size:.{precision}f}{units[unit_index]}"
222
+ elif size == int(size):
223
+ return f"{int(size)}{units[unit_index]}"
224
+ else:
225
+ # Auto precision (up to 2 decimal places, remove trailing zeros)
226
+ return f"{size:.2f}".rstrip("0").rstrip(".") + units[unit_index]
227
+
228
+
229
+ # Convenience constants
230
+ ZERO = BytesSize(0)
231
+ KB = BytesSize(1024)
232
+ MB = BytesSize(1024 * 1024)
233
+ GB = BytesSize(1024 * 1024 * 1024)
234
+ TB = BytesSize(1024 * 1024 * 1024 * 1024)
@@ -0,0 +1,291 @@
1
+ """
2
+ Enterprise-grade Connection String implementation for RocketWelder SDK.
3
+ Matches C# ConnectionString struct functionality.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import contextlib
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum, Flag, auto
11
+ from typing import Any
12
+
13
+ from .bytes_size import BytesSize
14
+
15
+
16
+ class Protocol(Flag):
17
+ """Protocol flags for connection types."""
18
+
19
+ NONE = 0
20
+ SHM = auto() # Shared memory
21
+ MJPEG = auto() # Motion JPEG
22
+ HTTP = auto() # HTTP protocol
23
+ TCP = auto() # TCP protocol
24
+ FILE = auto() # File protocol
25
+
26
+
27
+ class ConnectionMode(Enum):
28
+ """Connection mode for duplex/one-way communication."""
29
+
30
+ ONE_WAY = "OneWay"
31
+ DUPLEX = "Duplex"
32
+
33
+ def __str__(self) -> str:
34
+ """String representation."""
35
+ return self.value
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ConnectionString:
40
+ """
41
+ Immutable connection string representation for RocketWelder SDK.
42
+
43
+ Supports parsing connection strings like:
44
+ - shm://buffer_name?size=256MB&metadata=4KB&mode=Duplex
45
+ - mjpeg://192.168.1.100:8080
46
+ - mjpeg+http://camera.local:80
47
+ - file:///path/to/video.mp4?loop=true
48
+ """
49
+
50
+ protocol: Protocol
51
+ host: str | None = None
52
+ port: int | None = None
53
+ buffer_name: str | None = None
54
+ file_path: str | None = None
55
+ parameters: dict[str, str] = field(default_factory=dict)
56
+ buffer_size: BytesSize = field(default_factory=lambda: BytesSize.parse("256MB"))
57
+ metadata_size: BytesSize = field(default_factory=lambda: BytesSize.parse("4KB"))
58
+ connection_mode: ConnectionMode = ConnectionMode.ONE_WAY
59
+ timeout_ms: int = 5000
60
+
61
+ @classmethod
62
+ def parse(cls, connection_string: str) -> ConnectionString:
63
+ """
64
+ Parse a connection string into a ConnectionString object.
65
+
66
+ Args:
67
+ connection_string: Connection string to parse
68
+
69
+ Returns:
70
+ ConnectionString instance
71
+
72
+ Raises:
73
+ ValueError: If the connection string format is invalid
74
+ """
75
+ if not connection_string:
76
+ raise ValueError("Connection string cannot be empty")
77
+
78
+ # Handle special protocols
79
+ if "://" not in connection_string:
80
+ raise ValueError(f"Invalid connection string format: {connection_string}")
81
+
82
+ # Split protocol and remainder
83
+ protocol_str, remainder = connection_string.split("://", 1)
84
+ protocol = cls._parse_protocol(protocol_str)
85
+
86
+ # Parse based on protocol type
87
+ if protocol == Protocol.SHM:
88
+ return cls._parse_shm(protocol, remainder)
89
+ elif protocol == Protocol.FILE:
90
+ return cls._parse_file(protocol, remainder)
91
+ elif bool(protocol & Protocol.MJPEG): # type: ignore[operator]
92
+ return cls._parse_mjpeg(protocol, remainder)
93
+ else:
94
+ raise ValueError(f"Unsupported protocol: {protocol_str}")
95
+
96
+ @classmethod
97
+ def _parse_protocol(cls, protocol_str: str) -> Protocol:
98
+ """Parse protocol string into Protocol flags."""
99
+ protocol_str = protocol_str.lower()
100
+
101
+ # Handle combined protocols (e.g., mjpeg+http)
102
+ if "+" in protocol_str:
103
+ parts = protocol_str.split("+")
104
+ result = Protocol.NONE
105
+ for part in parts:
106
+ result |= cls._get_single_protocol(part)
107
+ return result
108
+ else:
109
+ return cls._get_single_protocol(protocol_str)
110
+
111
+ @classmethod
112
+ def _get_single_protocol(cls, protocol_str: str) -> Protocol:
113
+ """Get a single protocol from string."""
114
+ protocol_map = {
115
+ "shm": Protocol.SHM,
116
+ "mjpeg": Protocol.MJPEG,
117
+ "http": Protocol.HTTP,
118
+ "tcp": Protocol.TCP,
119
+ "file": Protocol.FILE,
120
+ }
121
+
122
+ protocol = protocol_map.get(protocol_str, Protocol.NONE)
123
+ if protocol == Protocol.NONE:
124
+ raise ValueError(f"Unknown protocol: {protocol_str}")
125
+ return protocol
126
+
127
+ @classmethod
128
+ def _parse_shm(cls, protocol: Protocol, remainder: str) -> ConnectionString:
129
+ """Parse shared memory connection string."""
130
+ # Split buffer name and query parameters
131
+ if "?" in remainder:
132
+ buffer_name, query_string = remainder.split("?", 1)
133
+ params = cls._parse_query_params(query_string)
134
+ else:
135
+ buffer_name = remainder
136
+ params = {}
137
+
138
+ # Parse parameters
139
+ buffer_size = BytesSize.parse("256MB")
140
+ metadata_size = BytesSize.parse("4KB")
141
+ connection_mode = ConnectionMode.ONE_WAY
142
+ timeout_ms = 5000
143
+
144
+ if "size" in params:
145
+ buffer_size = BytesSize.parse(params["size"])
146
+ if "metadata" in params:
147
+ metadata_size = BytesSize.parse(params["metadata"])
148
+ if "mode" in params:
149
+ mode_str = params["mode"].upper()
150
+ if mode_str == "DUPLEX":
151
+ connection_mode = ConnectionMode.DUPLEX
152
+ elif mode_str == "ONEWAY" or mode_str == "ONE_WAY":
153
+ connection_mode = ConnectionMode.ONE_WAY
154
+ if "timeout" in params:
155
+ with contextlib.suppress(ValueError):
156
+ timeout_ms = int(params["timeout"])
157
+
158
+ return cls(
159
+ protocol=protocol,
160
+ buffer_name=buffer_name,
161
+ buffer_size=buffer_size,
162
+ metadata_size=metadata_size,
163
+ connection_mode=connection_mode,
164
+ timeout_ms=timeout_ms,
165
+ )
166
+
167
+ @classmethod
168
+ def _parse_file(cls, protocol: Protocol, remainder: str) -> ConnectionString:
169
+ """Parse file protocol connection string."""
170
+ # Split file path and query parameters
171
+ if "?" in remainder:
172
+ file_path, query_string = remainder.split("?", 1)
173
+ params = cls._parse_query_params(query_string)
174
+ else:
175
+ file_path = remainder
176
+ params = {}
177
+
178
+ # Handle file:///absolute/path and file://relative/path
179
+ if not file_path.startswith("/"):
180
+ file_path = "/" + file_path
181
+
182
+ # Parse common parameters
183
+ connection_mode = ConnectionMode.ONE_WAY
184
+ timeout_ms = 5000
185
+
186
+ if "mode" in params:
187
+ mode_str = params["mode"].upper()
188
+ if mode_str == "DUPLEX":
189
+ connection_mode = ConnectionMode.DUPLEX
190
+ elif mode_str in ("ONEWAY", "ONE_WAY"):
191
+ connection_mode = ConnectionMode.ONE_WAY
192
+ if "timeout" in params:
193
+ with contextlib.suppress(ValueError):
194
+ timeout_ms = int(params["timeout"])
195
+
196
+ return cls(
197
+ protocol=protocol,
198
+ file_path=file_path,
199
+ parameters=params,
200
+ connection_mode=connection_mode,
201
+ timeout_ms=timeout_ms,
202
+ )
203
+
204
+ @classmethod
205
+ def _parse_mjpeg(cls, protocol: Protocol, remainder: str) -> ConnectionString:
206
+ """Parse MJPEG connection string."""
207
+ # Split host:port and query parameters
208
+ if "?" in remainder:
209
+ host_port, query_string = remainder.split("?", 1)
210
+ params = cls._parse_query_params(query_string)
211
+ else:
212
+ host_port = remainder
213
+ params = {}
214
+
215
+ # Parse host:port format
216
+ if ":" in host_port:
217
+ host, port_str = host_port.rsplit(":", 1)
218
+ try:
219
+ port = int(port_str)
220
+ except ValueError as e:
221
+ raise ValueError(f"Invalid port number: {port_str}") from e
222
+ else:
223
+ host = host_port
224
+ # Default ports based on protocol
225
+ port = 80 if Protocol.HTTP in protocol else 8080
226
+
227
+ return cls(protocol=protocol, host=host, port=port, parameters=params)
228
+
229
+ @classmethod
230
+ def _parse_query_params(cls, query_string: str) -> dict[str, str]:
231
+ """Parse query parameters from string."""
232
+ params: dict[str, str] = {}
233
+ if not query_string:
234
+ return params
235
+
236
+ pairs = query_string.split("&")
237
+ for pair in pairs:
238
+ if "=" in pair:
239
+ key, value = pair.split("=", 1)
240
+ params[key.lower()] = value
241
+
242
+ return params
243
+
244
+ def __str__(self) -> str:
245
+ """Convert to connection string format."""
246
+ # Format protocol
247
+ protocol_parts = []
248
+ if self.protocol & Protocol.SHM:
249
+ protocol_parts.append("shm")
250
+ if self.protocol & Protocol.FILE:
251
+ protocol_parts.append("file")
252
+ if self.protocol & Protocol.MJPEG:
253
+ protocol_parts.append("mjpeg")
254
+ if self.protocol & Protocol.HTTP:
255
+ protocol_parts.append("http")
256
+ if self.protocol & Protocol.TCP:
257
+ protocol_parts.append("tcp")
258
+
259
+ protocol_str = "+".join(protocol_parts)
260
+
261
+ # Format based on protocol type
262
+ if self.protocol == Protocol.SHM:
263
+ params = [
264
+ f"size={self.buffer_size}",
265
+ f"metadata={self.metadata_size}",
266
+ f"mode={self.connection_mode.value}",
267
+ ]
268
+ if self.timeout_ms != 5000:
269
+ params.append(f"timeout={self.timeout_ms}")
270
+
271
+ return f"{protocol_str}://{self.buffer_name}?{'&'.join(params)}"
272
+ elif self.protocol == Protocol.FILE:
273
+ query_string = ""
274
+ if self.parameters:
275
+ query_string = "?" + "&".join(f"{k}={v}" for k, v in self.parameters.items())
276
+ return f"{protocol_str}://{self.file_path}{query_string}"
277
+ else:
278
+ return f"{protocol_str}://{self.host}:{self.port}"
279
+
280
+ def to_dict(self) -> dict[str, Any]:
281
+ """Convert to dictionary representation."""
282
+ return {
283
+ "protocol": str(self.protocol),
284
+ "host": self.host,
285
+ "port": self.port,
286
+ "buffer_name": self.buffer_name,
287
+ "buffer_size": str(self.buffer_size),
288
+ "metadata_size": str(self.metadata_size),
289
+ "connection_mode": self.connection_mode.value,
290
+ "timeout_ms": self.timeout_ms,
291
+ }