rocket-welder-sdk 1.0.5__py3-none-any.whl → 1.1.25__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.
@@ -1,8 +1,56 @@
1
1
  """
2
- Rocket Welder SDK - Python client library for RocketWelder video streaming services.
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.
3
5
  """
4
6
 
5
- from .client import Client
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 .gst_metadata import GstCaps, GstMetadata
14
+ from .rocket_welder_client import RocketWelderClient
15
+
16
+ # Alias for backward compatibility
17
+ Client = RocketWelderClient
18
+
19
+ __version__ = "1.1.0"
20
+
21
+ # Configure library logger with NullHandler (best practice for libraries)
22
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
23
+
24
+ # Configure from environment variable and propagate to zerobuffer
25
+ _log_level = os.environ.get("ROCKET_WELDER_LOG_LEVEL")
26
+ if _log_level:
27
+ try:
28
+ # Set rocket-welder-sdk log level
29
+ logging.getLogger(__name__).setLevel(getattr(logging, _log_level.upper()))
30
+
31
+ # Propagate to zerobuffer if not already set
32
+ if not os.environ.get("ZEROBUFFER_LOG_LEVEL"):
33
+ os.environ["ZEROBUFFER_LOG_LEVEL"] = _log_level
34
+ # Also configure zerobuffer logger if already imported
35
+ zerobuffer_logger = logging.getLogger("zerobuffer")
36
+ zerobuffer_logger.setLevel(getattr(logging, _log_level.upper()))
37
+ except AttributeError:
38
+ pass # Invalid log level, ignore
6
39
 
7
- __version__ = "1.0.0"
8
- __all__ = ["Client"]
40
+ __all__ = [
41
+ # Core types
42
+ "BytesSize",
43
+ "Client", # Backward compatibility
44
+ "ConnectionMode",
45
+ "ConnectionString",
46
+ "DuplexShmController",
47
+ # GStreamer metadata
48
+ "GstCaps",
49
+ "GstMetadata",
50
+ # Controllers
51
+ "IController",
52
+ "OneWayShmController",
53
+ "Protocol",
54
+ # Main client
55
+ "RocketWelderClient",
56
+ ]
@@ -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,232 @@
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
+
25
+
26
+ class ConnectionMode(Enum):
27
+ """Connection mode for duplex/one-way communication."""
28
+
29
+ ONE_WAY = "OneWay"
30
+ DUPLEX = "Duplex"
31
+
32
+ def __str__(self) -> str:
33
+ """String representation."""
34
+ return self.value
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ConnectionString:
39
+ """
40
+ Immutable connection string representation for RocketWelder SDK.
41
+
42
+ Supports parsing connection strings like:
43
+ - shm://buffer_name?size=256MB&metadata=4KB&mode=Duplex
44
+ - mjpeg://192.168.1.100:8080
45
+ - mjpeg+http://camera.local:80
46
+ """
47
+
48
+ protocol: Protocol
49
+ host: str | None = None
50
+ port: int | None = None
51
+ buffer_name: str | None = None
52
+ buffer_size: BytesSize = field(default_factory=lambda: BytesSize.parse("256MB"))
53
+ metadata_size: BytesSize = field(default_factory=lambda: BytesSize.parse("4KB"))
54
+ connection_mode: ConnectionMode = ConnectionMode.ONE_WAY
55
+ timeout_ms: int = 5000
56
+
57
+ @classmethod
58
+ def parse(cls, connection_string: str) -> ConnectionString:
59
+ """
60
+ Parse a connection string into a ConnectionString object.
61
+
62
+ Args:
63
+ connection_string: Connection string to parse
64
+
65
+ Returns:
66
+ ConnectionString instance
67
+
68
+ Raises:
69
+ ValueError: If the connection string format is invalid
70
+ """
71
+ if not connection_string:
72
+ raise ValueError("Connection string cannot be empty")
73
+
74
+ # Handle special protocols
75
+ if "://" not in connection_string:
76
+ raise ValueError(f"Invalid connection string format: {connection_string}")
77
+
78
+ # Split protocol and remainder
79
+ protocol_str, remainder = connection_string.split("://", 1)
80
+ protocol = cls._parse_protocol(protocol_str)
81
+
82
+ # Parse based on protocol type
83
+ if protocol == Protocol.SHM:
84
+ return cls._parse_shm(protocol, remainder)
85
+ elif bool(protocol & Protocol.MJPEG): # type: ignore[operator]
86
+ return cls._parse_mjpeg(protocol, remainder)
87
+ else:
88
+ raise ValueError(f"Unsupported protocol: {protocol_str}")
89
+
90
+ @classmethod
91
+ def _parse_protocol(cls, protocol_str: str) -> Protocol:
92
+ """Parse protocol string into Protocol flags."""
93
+ protocol_str = protocol_str.lower()
94
+
95
+ # Handle combined protocols (e.g., mjpeg+http)
96
+ if "+" in protocol_str:
97
+ parts = protocol_str.split("+")
98
+ result = Protocol.NONE
99
+ for part in parts:
100
+ result |= cls._get_single_protocol(part)
101
+ return result
102
+ else:
103
+ return cls._get_single_protocol(protocol_str)
104
+
105
+ @classmethod
106
+ def _get_single_protocol(cls, protocol_str: str) -> Protocol:
107
+ """Get a single protocol from string."""
108
+ protocol_map = {
109
+ "shm": Protocol.SHM,
110
+ "mjpeg": Protocol.MJPEG,
111
+ "http": Protocol.HTTP,
112
+ "tcp": Protocol.TCP,
113
+ }
114
+
115
+ protocol = protocol_map.get(protocol_str, Protocol.NONE)
116
+ if protocol == Protocol.NONE:
117
+ raise ValueError(f"Unknown protocol: {protocol_str}")
118
+ return protocol
119
+
120
+ @classmethod
121
+ def _parse_shm(cls, protocol: Protocol, remainder: str) -> ConnectionString:
122
+ """Parse shared memory connection string."""
123
+ # Split buffer name and query parameters
124
+ if "?" in remainder:
125
+ buffer_name, query_string = remainder.split("?", 1)
126
+ params = cls._parse_query_params(query_string)
127
+ else:
128
+ buffer_name = remainder
129
+ params = {}
130
+
131
+ # Parse parameters
132
+ buffer_size = BytesSize.parse("256MB")
133
+ metadata_size = BytesSize.parse("4KB")
134
+ connection_mode = ConnectionMode.ONE_WAY
135
+ timeout_ms = 5000
136
+
137
+ if "size" in params:
138
+ buffer_size = BytesSize.parse(params["size"])
139
+ if "metadata" in params:
140
+ metadata_size = BytesSize.parse(params["metadata"])
141
+ if "mode" in params:
142
+ mode_str = params["mode"].upper()
143
+ if mode_str == "DUPLEX":
144
+ connection_mode = ConnectionMode.DUPLEX
145
+ elif mode_str == "ONEWAY" or mode_str == "ONE_WAY":
146
+ connection_mode = ConnectionMode.ONE_WAY
147
+ if "timeout" in params:
148
+ with contextlib.suppress(ValueError):
149
+ timeout_ms = int(params["timeout"])
150
+
151
+ return cls(
152
+ protocol=protocol,
153
+ buffer_name=buffer_name,
154
+ buffer_size=buffer_size,
155
+ metadata_size=metadata_size,
156
+ connection_mode=connection_mode,
157
+ timeout_ms=timeout_ms,
158
+ )
159
+
160
+ @classmethod
161
+ def _parse_mjpeg(cls, protocol: Protocol, remainder: str) -> ConnectionString:
162
+ """Parse MJPEG connection string."""
163
+ # Parse host:port format
164
+ if ":" in remainder:
165
+ host, port_str = remainder.rsplit(":", 1)
166
+ try:
167
+ port = int(port_str)
168
+ except ValueError as e:
169
+ raise ValueError(f"Invalid port number: {port_str}") from e
170
+ else:
171
+ host = remainder
172
+ # Default ports based on protocol
173
+ port = 80 if Protocol.HTTP in protocol else 8080
174
+
175
+ return cls(protocol=protocol, host=host, port=port)
176
+
177
+ @classmethod
178
+ def _parse_query_params(cls, query_string: str) -> dict[str, str]:
179
+ """Parse query parameters from string."""
180
+ params: dict[str, str] = {}
181
+ if not query_string:
182
+ return params
183
+
184
+ pairs = query_string.split("&")
185
+ for pair in pairs:
186
+ if "=" in pair:
187
+ key, value = pair.split("=", 1)
188
+ params[key.lower()] = value
189
+
190
+ return params
191
+
192
+ def __str__(self) -> str:
193
+ """Convert to connection string format."""
194
+ # Format protocol
195
+ protocol_parts = []
196
+ if self.protocol & Protocol.SHM:
197
+ protocol_parts.append("shm")
198
+ if self.protocol & Protocol.MJPEG:
199
+ protocol_parts.append("mjpeg")
200
+ if self.protocol & Protocol.HTTP:
201
+ protocol_parts.append("http")
202
+ if self.protocol & Protocol.TCP:
203
+ protocol_parts.append("tcp")
204
+
205
+ protocol_str = "+".join(protocol_parts)
206
+
207
+ # Format based on protocol type
208
+ if self.protocol == Protocol.SHM:
209
+ params = [
210
+ f"size={self.buffer_size}",
211
+ f"metadata={self.metadata_size}",
212
+ f"mode={self.connection_mode.value}",
213
+ ]
214
+ if self.timeout_ms != 5000:
215
+ params.append(f"timeout={self.timeout_ms}")
216
+
217
+ return f"{protocol_str}://{self.buffer_name}?{'&'.join(params)}"
218
+ else:
219
+ return f"{protocol_str}://{self.host}:{self.port}"
220
+
221
+ def to_dict(self) -> dict[str, Any]:
222
+ """Convert to dictionary representation."""
223
+ return {
224
+ "protocol": str(self.protocol),
225
+ "host": self.host,
226
+ "port": self.port,
227
+ "buffer_name": self.buffer_name,
228
+ "buffer_size": str(self.buffer_size),
229
+ "metadata_size": str(self.metadata_size),
230
+ "connection_mode": self.connection_mode.value,
231
+ "timeout_ms": self.timeout_ms,
232
+ }