rocket-welder-sdk 1.1.0__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.
@@ -4,6 +4,9 @@ RocketWelder SDK - Enterprise-grade Python client library for video streaming se
4
4
  High-performance video streaming using shared memory (ZeroBuffer) for zero-copy operations.
5
5
  """
6
6
 
7
+ import logging
8
+ import os
9
+
7
10
  from .bytes_size import BytesSize
8
11
  from .connection_string import ConnectionMode, ConnectionString, Protocol
9
12
  from .controllers import DuplexShmController, IController, OneWayShmController
@@ -15,6 +18,25 @@ Client = RocketWelderClient
15
18
 
16
19
  __version__ = "1.1.0"
17
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
39
+
18
40
  __all__ = [
19
41
  # Core types
20
42
  "BytesSize",
@@ -14,6 +14,7 @@ from typing import Any, Callable
14
14
  import numpy as np
15
15
  from zerobuffer import BufferConfig, Frame, Reader, Writer
16
16
  from zerobuffer.duplex import DuplexChannelFactory, IImmutableDuplexServer
17
+ from zerobuffer.exceptions import WriterDeadException
17
18
 
18
19
  from .connection_string import ConnectionMode, ConnectionString, Protocol
19
20
  from .gst_metadata import GstCaps, GstMetadata
@@ -21,6 +22,9 @@ from .gst_metadata import GstCaps, GstMetadata
21
22
  # Type alias for OpenCV Mat
22
23
  Mat = np.ndarray[Any, Any]
23
24
 
25
+ # Module logger
26
+ logger = logging.getLogger(__name__)
27
+
24
28
 
25
29
  class IController(ABC):
26
30
  """Abstract base class for controllers."""
@@ -63,13 +67,12 @@ class OneWayShmController(IController):
63
67
  as a zerosink, allowing zero-copy frame reception.
64
68
  """
65
69
 
66
- def __init__(self, connection: ConnectionString, logger: logging.Logger | None = None):
70
+ def __init__(self, connection: ConnectionString):
67
71
  """
68
72
  Initialize the one-way controller.
69
73
 
70
74
  Args:
71
75
  connection: Connection string configuration
72
- logger: Optional logger instance
73
76
  """
74
77
  if connection.protocol != Protocol.SHM:
75
78
  raise ValueError(
@@ -77,8 +80,6 @@ class OneWayShmController(IController):
77
80
  )
78
81
 
79
82
  self._connection = connection
80
- self._logger = logger or logging.getLogger(__name__)
81
- self._reader_logger = logging.getLogger(f"{__name__}.Reader")
82
83
  self._reader: Reader | None = None
83
84
  self._gst_caps: GstCaps | None = None
84
85
  self._metadata: GstMetadata | None = None
@@ -108,9 +109,7 @@ class OneWayShmController(IController):
108
109
  if self._is_running:
109
110
  raise RuntimeError("Controller is already running")
110
111
 
111
- self._logger.debug(
112
- "Starting OneWayShmController for buffer '%s'", self._connection.buffer_name
113
- )
112
+ logger.debug("Starting OneWayShmController for buffer '%s'", self._connection.buffer_name)
114
113
  self._is_running = True
115
114
  self._cancellation_token = cancellation_token
116
115
 
@@ -124,9 +123,9 @@ class OneWayShmController(IController):
124
123
  # Pass logger to Reader for better debugging
125
124
  if not self._connection.buffer_name:
126
125
  raise ValueError("Buffer name is required for shared memory connection")
127
- self._reader = Reader(self._connection.buffer_name, config, logger=self._reader_logger)
126
+ self._reader = Reader(self._connection.buffer_name, config)
128
127
 
129
- self._logger.info(
128
+ logger.info(
130
129
  "Created shared memory buffer '%s' with size %s and metadata %s",
131
130
  self._connection.buffer_name,
132
131
  self._connection.buffer_size,
@@ -146,7 +145,7 @@ class OneWayShmController(IController):
146
145
  if not self._is_running:
147
146
  return
148
147
 
149
- self._logger.debug("Stopping controller for buffer '%s'", self._connection.buffer_name)
148
+ logger.debug("Stopping controller for buffer '%s'", self._connection.buffer_name)
150
149
  self._is_running = False
151
150
 
152
151
  # Wait for worker thread to finish
@@ -160,7 +159,7 @@ class OneWayShmController(IController):
160
159
  self._reader = None
161
160
 
162
161
  self._worker_thread = None
163
- self._logger.info("Stopped controller for buffer '%s'", self._connection.buffer_name)
162
+ logger.info("Stopped controller for buffer '%s'", self._connection.buffer_name)
164
163
 
165
164
  def _process_frames(self, on_frame: Callable[[Mat], None]) -> None:
166
165
  """
@@ -193,35 +192,39 @@ class OneWayShmController(IController):
193
192
  if mat is not None:
194
193
  on_frame(mat)
195
194
 
195
+ except WriterDeadException:
196
+ # Writer has disconnected gracefully
197
+ logger.info(
198
+ "Writer disconnected gracefully from buffer '%s'",
199
+ self._connection.buffer_name,
200
+ )
201
+ self._is_running = False
202
+ break
196
203
  except Exception as e:
197
204
  # Log specific error types like C#
198
205
  error_type = type(e).__name__
199
- if "ReaderDead" in error_type or "WriterDead" in error_type:
200
- self._logger.info(
201
- "Writer disconnected from buffer '%s'", self._connection.buffer_name
206
+ if "ReaderDead" in error_type:
207
+ logger.info(
208
+ "Reader disconnected from buffer '%s'", self._connection.buffer_name
202
209
  )
203
210
  self._is_running = False
204
211
  break
205
212
  elif "BufferFull" in error_type:
206
- self._logger.error(
207
- "Buffer full on '%s': %s", self._connection.buffer_name, e
208
- )
213
+ logger.error("Buffer full on '%s': %s", self._connection.buffer_name, e)
209
214
  if not self._is_running:
210
215
  break
211
216
  elif "FrameTooLarge" in error_type:
212
- self._logger.error(
213
- "Frame too large on '%s': %s", self._connection.buffer_name, e
214
- )
217
+ logger.error("Frame too large on '%s': %s", self._connection.buffer_name, e)
215
218
  if not self._is_running:
216
219
  break
217
220
  elif "ZeroBuffer" in error_type:
218
- self._logger.error(
221
+ logger.error(
219
222
  "ZeroBuffer error on '%s': %s", self._connection.buffer_name, e
220
223
  )
221
224
  if not self._is_running:
222
225
  break
223
226
  else:
224
- self._logger.error(
227
+ logger.error(
225
228
  "Unexpected error processing frame from buffer '%s': %s",
226
229
  self._connection.buffer_name,
227
230
  e,
@@ -230,7 +233,7 @@ class OneWayShmController(IController):
230
233
  break
231
234
 
232
235
  except Exception as e:
233
- self._logger.error("Fatal error in frame processing loop: %s", e)
236
+ logger.error("Fatal error in frame processing loop: %s", e)
234
237
  self._is_running = False
235
238
 
236
239
  def _on_first_frame(self, on_frame: Callable[[Mat], None]) -> None:
@@ -258,53 +261,53 @@ class OneWayShmController(IController):
258
261
  if metadata_bytes:
259
262
  try:
260
263
  # Log raw metadata for debugging
261
- self._logger.debug(
264
+ logger.debug(
262
265
  "Raw metadata: %d bytes, type=%s, first 100 bytes: %s",
263
266
  len(metadata_bytes),
264
267
  type(metadata_bytes),
265
- bytes(metadata_bytes[:min(100, len(metadata_bytes))]),
268
+ bytes(metadata_bytes[: min(100, len(metadata_bytes))]),
266
269
  )
267
-
270
+
268
271
  # Convert memoryview to bytes if needed
269
272
  if isinstance(metadata_bytes, memoryview):
270
273
  metadata_bytes = bytes(metadata_bytes)
271
-
274
+
272
275
  # Decode UTF-8
273
276
  metadata_str = metadata_bytes.decode("utf-8")
274
-
277
+
275
278
  # Check if metadata is empty or all zeros
276
- if not metadata_str or metadata_str == '\x00' * len(metadata_str):
277
- self._logger.warning("Metadata is empty or all zeros, skipping")
279
+ if not metadata_str or metadata_str == "\x00" * len(metadata_str):
280
+ logger.warning("Metadata is empty or all zeros, skipping")
278
281
  continue
279
-
282
+
280
283
  # Find the start of JSON (skip any null bytes at the beginning)
281
- json_start = metadata_str.find('{')
284
+ json_start = metadata_str.find("{")
282
285
  if json_start == -1:
283
- self._logger.warning("No JSON found in metadata: %r", metadata_str[:100])
286
+ logger.warning("No JSON found in metadata: %r", metadata_str[:100])
284
287
  continue
285
-
288
+
286
289
  if json_start > 0:
287
- self._logger.debug("Skipping %d bytes before JSON", json_start)
290
+ logger.debug("Skipping %d bytes before JSON", json_start)
288
291
  metadata_str = metadata_str[json_start:]
289
-
292
+
290
293
  # Find the end of JSON (handle null padding)
291
- json_end = metadata_str.rfind('}')
294
+ json_end = metadata_str.rfind("}")
292
295
  if json_end != -1 and json_end < len(metadata_str) - 1:
293
- metadata_str = metadata_str[:json_end + 1]
294
-
296
+ metadata_str = metadata_str[: json_end + 1]
297
+
295
298
  metadata_json = json.loads(metadata_str)
296
299
  self._metadata = GstMetadata.from_json(metadata_json)
297
300
  self._gst_caps = self._metadata.caps
298
- self._logger.info(
301
+ logger.info(
299
302
  "Received metadata from buffer '%s': %s",
300
303
  self._connection.buffer_name,
301
304
  self._gst_caps,
302
305
  )
303
306
  except Exception as e:
304
- self._logger.error("Failed to parse metadata: %s", e)
307
+ logger.error("Failed to parse metadata: %s", e)
305
308
  # Log the actual metadata content for debugging
306
309
  if metadata_bytes:
307
- self._logger.debug("Metadata content: %r", metadata_bytes[:200])
310
+ logger.debug("Metadata content: %r", metadata_bytes[:200])
308
311
  # Don't continue without metadata
309
312
  continue
310
313
 
@@ -314,17 +317,24 @@ class OneWayShmController(IController):
314
317
  on_frame(mat)
315
318
  return # Successfully processed first frame
316
319
 
320
+ except WriterDeadException:
321
+ self._is_running = False
322
+ logger.info(
323
+ "Writer disconnected gracefully while waiting for first frame on buffer '%s'",
324
+ self._connection.buffer_name,
325
+ )
326
+ raise
317
327
  except Exception as e:
318
328
  error_type = type(e).__name__
319
- if "ReaderDead" in error_type or "WriterDead" in error_type:
329
+ if "ReaderDead" in error_type:
320
330
  self._is_running = False
321
- self._logger.info(
322
- "Writer disconnected while waiting for first frame on buffer '%s'",
331
+ logger.info(
332
+ "Reader disconnected while waiting for first frame on buffer '%s'",
323
333
  self._connection.buffer_name,
324
334
  )
325
335
  raise
326
336
  else:
327
- self._logger.error(
337
+ logger.error(
328
338
  "Error waiting for first frame on buffer '%s': %s",
329
339
  self._connection.buffer_name,
330
340
  e,
@@ -367,7 +377,7 @@ class OneWayShmController(IController):
367
377
  # Check data size matches expected
368
378
  expected_size = height * width * channels
369
379
  if len(data) != expected_size:
370
- self._logger.error(
380
+ logger.error(
371
381
  "Data size mismatch. Expected %d bytes for %dx%d with %d channels, got %d",
372
382
  expected_size,
373
383
  width,
@@ -386,17 +396,48 @@ class OneWayShmController(IController):
386
396
  elif channels == 4:
387
397
  mat = data.reshape((height, width, 4))
388
398
  else:
389
- self._logger.error("Unsupported channel count: %d", channels)
399
+ logger.error("Unsupported channel count: %d", channels)
390
400
  return None
391
401
 
392
402
  return mat # type: ignore[no-any-return]
393
403
 
394
- # No caps available
395
- self._logger.error("No GstCaps available for frame conversion")
404
+ # No caps available - try to infer from frame size
405
+ logger.warning("No GstCaps available, attempting to infer from frame size")
406
+
407
+ # Try common resolutions
408
+ frame_size = len(frame.data)
409
+ common_resolutions = [
410
+ (640, 480, 3), # VGA RGB
411
+ (640, 480, 4), # VGA RGBA
412
+ (1280, 720, 3), # 720p RGB
413
+ (1920, 1080, 3), # 1080p RGB
414
+ (640, 480, 1), # VGA Grayscale
415
+ ]
416
+
417
+ for width, height, channels in common_resolutions:
418
+ if frame_size == width * height * channels:
419
+ logger.info(f"Inferred resolution: {width}x{height} with {channels} channels")
420
+
421
+ # Create caps for future use
422
+ format_str = "RGB" if channels == 3 else "RGBA" if channels == 4 else "GRAY8"
423
+ self._gst_caps = GstCaps.from_simple(
424
+ width=width, height=height, format=format_str
425
+ )
426
+
427
+ # Create Mat
428
+ data = np.frombuffer(frame.data, dtype=np.uint8)
429
+ if channels == 3:
430
+ return data.reshape((height, width, 3)) # type: ignore[no-any-return]
431
+ elif channels == 1:
432
+ return data.reshape((height, width)) # type: ignore[no-any-return]
433
+ elif channels == 4:
434
+ return data.reshape((height, width, 4)) # type: ignore[no-any-return]
435
+
436
+ logger.error(f"Could not infer resolution for frame size {frame_size}")
396
437
  return None
397
438
 
398
439
  except Exception as e:
399
- self._logger.error("Failed to convert frame to Mat: %s", e)
440
+ logger.error("Failed to convert frame to Mat: %s", e)
400
441
  return None
401
442
 
402
443
  def _infer_caps_from_frame(self, mat: Mat) -> None:
@@ -412,12 +453,12 @@ class OneWayShmController(IController):
412
453
  shape = mat.shape
413
454
  if len(shape) == 2:
414
455
  # Grayscale
415
- self._gst_caps = GstCaps(format="GRAY8", width=shape[1], height=shape[0])
456
+ self._gst_caps = GstCaps.from_simple(width=shape[1], height=shape[0], format="GRAY8")
416
457
  elif len(shape) == 3:
417
458
  # Color image
418
- self._gst_caps = GstCaps(format="BGR", width=shape[1], height=shape[0])
459
+ self._gst_caps = GstCaps.from_simple(width=shape[1], height=shape[0], format="BGR")
419
460
 
420
- self._logger.info("Inferred caps from frame: %s", self._gst_caps)
461
+ logger.info("Inferred caps from frame: %s", self._gst_caps)
421
462
 
422
463
 
423
464
  class DuplexShmController(IController):
@@ -428,13 +469,12 @@ class DuplexShmController(IController):
428
469
  sending processed frames to another buffer.
429
470
  """
430
471
 
431
- def __init__(self, connection: ConnectionString, logger: logging.Logger | None = None):
472
+ def __init__(self, connection: ConnectionString):
432
473
  """
433
474
  Initialize the duplex controller.
434
475
 
435
476
  Args:
436
477
  connection: Connection string configuration
437
- logger: Optional logger instance
438
478
  """
439
479
  if connection.protocol != Protocol.SHM:
440
480
  raise ValueError(
@@ -447,7 +487,6 @@ class DuplexShmController(IController):
447
487
  )
448
488
 
449
489
  self._connection = connection
450
- self._logger = logger or logging.getLogger(__name__)
451
490
  self._duplex_server: IImmutableDuplexServer | None = None
452
491
  self._gst_caps: GstCaps | None = None
453
492
  self._metadata: GstMetadata | None = None
@@ -498,7 +537,7 @@ class DuplexShmController(IController):
498
537
  self._connection.buffer_name, config, timeout_seconds
499
538
  )
500
539
 
501
- self._logger.info(
540
+ logger.info(
502
541
  "Starting duplex server for channel '%s' with size %s and metadata %s",
503
542
  self._connection.buffer_name,
504
543
  self._connection.buffer_size,
@@ -514,7 +553,7 @@ class DuplexShmController(IController):
514
553
  if not self._is_running:
515
554
  return
516
555
 
517
- self._logger.info("Stopping DuplexShmController")
556
+ logger.info("Stopping DuplexShmController")
518
557
  self._is_running = False
519
558
 
520
559
  # Stop the duplex server
@@ -522,7 +561,7 @@ class DuplexShmController(IController):
522
561
  self._duplex_server.stop()
523
562
  self._duplex_server = None
524
563
 
525
- self._logger.info("DuplexShmController stopped")
564
+ logger.info("DuplexShmController stopped")
526
565
 
527
566
  def _on_metadata(self, metadata_bytes: bytes | memoryview) -> None:
528
567
  """
@@ -531,18 +570,29 @@ class DuplexShmController(IController):
531
570
  Args:
532
571
  metadata_bytes: Raw metadata bytes or memoryview
533
572
  """
573
+ logger.debug(
574
+ "_on_metadata called with %d bytes", len(metadata_bytes) if metadata_bytes else 0
575
+ )
534
576
  try:
535
577
  # Convert memoryview to bytes if needed
536
578
  if isinstance(metadata_bytes, memoryview):
537
579
  metadata_bytes = bytes(metadata_bytes)
538
-
580
+
581
+ # Log raw bytes for debugging
582
+ logger.debug(
583
+ "Raw metadata bytes (first 100): %r",
584
+ metadata_bytes[: min(100, len(metadata_bytes))],
585
+ )
586
+
539
587
  metadata_str = metadata_bytes.decode("utf-8")
588
+ logger.debug("Decoded metadata string: %r", metadata_str[: min(200, len(metadata_str))])
589
+
540
590
  metadata_json = json.loads(metadata_str)
541
591
  self._metadata = GstMetadata.from_json(metadata_json)
542
592
  self._gst_caps = self._metadata.caps
543
- self._logger.info("Received metadata: %s", self._metadata)
593
+ logger.info("Received metadata: %s", self._metadata)
544
594
  except Exception as e:
545
- self._logger.warning("Failed to parse metadata: %s", e)
595
+ logger.error("Failed to parse metadata: %s", e, exc_info=True)
546
596
 
547
597
  def _process_duplex_frame(self, request_frame: Frame, response_writer: Writer) -> None:
548
598
  """
@@ -552,8 +602,14 @@ class DuplexShmController(IController):
552
602
  request_frame: Input frame from the request
553
603
  response_writer: Writer for the response frame
554
604
  """
605
+ logger.debug(
606
+ "_process_duplex_frame called, frame_count=%d, has_gst_caps=%s",
607
+ self._frame_count,
608
+ self._gst_caps is not None,
609
+ )
555
610
  try:
556
611
  if not self._on_frame_callback:
612
+ logger.warning("No frame callback set")
557
613
  return
558
614
 
559
615
  self._frame_count += 1
@@ -561,6 +617,7 @@ class DuplexShmController(IController):
561
617
  # Convert input frame to Mat
562
618
  input_mat = self._frame_to_mat(request_frame)
563
619
  if input_mat is None:
620
+ logger.error("Failed to convert frame to Mat, gst_caps=%s", self._gst_caps)
564
621
  return
565
622
 
566
623
  # Get buffer for output frame - use context manager for RAII
@@ -585,7 +642,9 @@ class DuplexShmController(IController):
585
642
  )
586
643
  else:
587
644
  # Use same shape as input
588
- output_mat = np.frombuffer(output_buffer, dtype=np.uint8).reshape(input_mat.shape)
645
+ output_mat = np.frombuffer(output_buffer, dtype=np.uint8).reshape(
646
+ input_mat.shape
647
+ )
589
648
 
590
649
  # Call user's processing function
591
650
  self._on_frame_callback(input_mat, output_mat)
@@ -593,7 +652,7 @@ class DuplexShmController(IController):
593
652
  # Commit the response frame after buffer is released
594
653
  response_writer.commit_frame()
595
654
 
596
- self._logger.debug(
655
+ logger.debug(
597
656
  "Processed duplex frame %d (%dx%d)",
598
657
  self._frame_count,
599
658
  input_mat.shape[1],
@@ -601,7 +660,7 @@ class DuplexShmController(IController):
601
660
  )
602
661
 
603
662
  except Exception as e:
604
- self._logger.error("Error processing duplex frame: %s", e)
663
+ logger.error("Error processing duplex frame: %s", e)
605
664
 
606
665
  def _frame_to_mat(self, frame: Frame) -> Mat | None:
607
666
  """Convert frame to OpenCV Mat (reuse from OneWayShmController)."""
@@ -6,9 +6,13 @@ Matches C# GstCaps and GstMetadata functionality.
6
6
  from __future__ import annotations
7
7
 
8
8
  import json
9
- from dataclasses import dataclass, field
9
+ import re
10
+ from dataclasses import dataclass
10
11
  from typing import Any
11
12
 
13
+ import numpy as np
14
+ import numpy.typing as npt
15
+
12
16
 
13
17
  @dataclass
14
18
  class GstCaps:
@@ -16,172 +20,312 @@ class GstCaps:
16
20
  GStreamer capabilities representation.
17
21
 
18
22
  Represents video format capabilities including format, dimensions, framerate, etc.
23
+ Matches the C# GstCaps implementation with proper parsing and numpy integration.
19
24
  """
20
25
 
21
- format: str | None = None
22
- width: int | None = None
23
- height: int | None = None
24
- framerate: str | None = None # e.g., "30/1" or "25/1"
25
- pixel_aspect_ratio: str | None = None # e.g., "1/1"
26
- interlace_mode: str | None = None # e.g., "progressive"
26
+ width: int
27
+ height: int
28
+ format: str
29
+ depth_type: type[np.uint8] | type[np.uint16]
30
+ channels: int
31
+ bytes_per_pixel: int
32
+ framerate_num: int | None = None
33
+ framerate_den: int | None = None
34
+ interlace_mode: str | None = None
27
35
  colorimetry: str | None = None
28
- chroma_site: str | None = None
36
+ caps_string: str | None = None
37
+
38
+ @property
39
+ def frame_size(self) -> int:
40
+ """Calculate the expected frame size in bytes."""
41
+ return self.width * self.height * self.bytes_per_pixel
29
42
 
30
- # Additional fields can be stored here
31
- extra_fields: dict[str, Any] = field(default_factory=dict)
43
+ @property
44
+ def framerate(self) -> float | None:
45
+ """Get framerate as double (FPS)."""
46
+ if (
47
+ self.framerate_num is not None
48
+ and self.framerate_den is not None
49
+ and self.framerate_den > 0
50
+ ):
51
+ return self.framerate_num / self.framerate_den
52
+ return None
32
53
 
33
54
  @classmethod
34
- def from_dict(cls, data: dict[str, Any]) -> GstCaps:
55
+ def parse(cls, caps_string: str) -> GstCaps:
35
56
  """
36
- Create GstCaps from dictionary.
57
+ Parse GStreamer caps string.
58
+ Example: "video/x-raw, format=(string)RGB, width=(int)640, height=(int)480, framerate=(fraction)30/1"
37
59
 
38
60
  Args:
39
- data: Dictionary containing caps data
61
+ caps_string: GStreamer caps string
40
62
 
41
63
  Returns:
42
64
  GstCaps instance
65
+
66
+ Raises:
67
+ ValueError: If caps string is invalid
43
68
  """
44
- # Extract known fields
45
- caps = cls(
46
- format=data.get("format"),
47
- width=data.get("width"),
48
- height=data.get("height"),
49
- framerate=data.get("framerate"),
50
- pixel_aspect_ratio=data.get("pixel-aspect-ratio"),
51
- interlace_mode=data.get("interlace-mode"),
52
- colorimetry=data.get("colorimetry"),
53
- chroma_site=data.get("chroma-site"),
54
- )
69
+ if not caps_string or not caps_string.strip():
70
+ raise ValueError("Empty caps string")
55
71
 
56
- # Store any extra fields
57
- known_fields = {
58
- "format",
59
- "width",
60
- "height",
61
- "framerate",
62
- "pixel-aspect-ratio",
63
- "interlace-mode",
64
- "colorimetry",
65
- "chroma-site",
66
- }
72
+ caps_string = caps_string.strip()
67
73
 
68
- for key, value in data.items():
69
- if key not in known_fields:
70
- caps.extra_fields[key] = value
74
+ # Check if it's a video caps
75
+ if not caps_string.startswith("video/x-raw"):
76
+ raise ValueError(f"Not a video/x-raw caps string: {caps_string}")
71
77
 
72
- return caps
78
+ try:
79
+ # Parse width
80
+ width_match = re.search(r"width=\(int\)(\d+)", caps_string)
81
+ if not width_match:
82
+ raise ValueError("Missing width in caps string")
83
+ width = int(width_match.group(1))
84
+
85
+ # Parse height
86
+ height_match = re.search(r"height=\(int\)(\d+)", caps_string)
87
+ if not height_match:
88
+ raise ValueError("Missing height in caps string")
89
+ height = int(height_match.group(1))
90
+
91
+ # Parse format
92
+ format_match = re.search(r"format=\(string\)(\w+)", caps_string)
93
+ format_str = format_match.group(1) if format_match else "RGB"
94
+
95
+ # Parse framerate (optional)
96
+ framerate_num = None
97
+ framerate_den = None
98
+ framerate_match = re.search(r"framerate=\(fraction\)(\d+)/(\d+)", caps_string)
99
+ if framerate_match:
100
+ framerate_num = int(framerate_match.group(1))
101
+ framerate_den = int(framerate_match.group(2))
102
+
103
+ # Parse interlace mode (optional)
104
+ interlace_mode = None
105
+ interlace_match = re.search(r"interlace-mode=\(string\)(\w+)", caps_string)
106
+ if interlace_match:
107
+ interlace_mode = interlace_match.group(1)
108
+
109
+ # Parse colorimetry (optional)
110
+ colorimetry = None
111
+ colorimetry_match = re.search(r"colorimetry=\(string\)([\w:]+)", caps_string)
112
+ if colorimetry_match:
113
+ colorimetry = colorimetry_match.group(1)
114
+
115
+ # Map format to numpy dtype and get channel info
116
+ depth_type, channels, bytes_per_pixel = cls._map_gstreamer_format_to_numpy(format_str)
117
+
118
+ return cls(
119
+ width=width,
120
+ height=height,
121
+ format=format_str,
122
+ depth_type=depth_type,
123
+ channels=channels,
124
+ bytes_per_pixel=bytes_per_pixel,
125
+ framerate_num=framerate_num,
126
+ framerate_den=framerate_den,
127
+ interlace_mode=interlace_mode,
128
+ colorimetry=colorimetry,
129
+ caps_string=caps_string,
130
+ )
131
+ except Exception as e:
132
+ raise ValueError(f"Failed to parse caps string: {caps_string}") from e
73
133
 
74
134
  @classmethod
75
- def from_string(cls, caps_string: str) -> GstCaps:
135
+ def from_simple(cls, width: int, height: int, format: str = "RGB") -> GstCaps:
76
136
  """
77
- Parse GStreamer caps string.
137
+ Create GstCaps from simple parameters.
78
138
 
79
139
  Args:
80
- caps_string: GStreamer caps string (e.g., "video/x-raw,format=RGB,width=640,height=480")
140
+ width: Frame width
141
+ height: Frame height
142
+ format: Pixel format (default: "RGB")
81
143
 
82
144
  Returns:
83
145
  GstCaps instance
84
146
  """
85
- if not caps_string:
86
- return cls()
147
+ depth_type, channels, bytes_per_pixel = cls._map_gstreamer_format_to_numpy(format)
148
+ return cls(
149
+ width=width,
150
+ height=height,
151
+ format=format,
152
+ depth_type=depth_type,
153
+ channels=channels,
154
+ bytes_per_pixel=bytes_per_pixel,
155
+ )
87
156
 
88
- # Remove media type prefix if present
89
- if "/" in caps_string and "," in caps_string:
90
- # Has media type prefix (e.g., "video/x-raw,format=RGB")
91
- _, params = caps_string.split(",", 1)
92
- else:
93
- # No media type prefix (e.g., "format=RGB,width=320")
94
- params = caps_string
95
-
96
- # Parse parameters
97
- data: dict[str, Any] = {}
98
- for param in params.split(","):
99
- if "=" in param:
100
- key, value = param.split("=", 1)
101
- key = key.strip()
102
- value_str = value.strip()
103
-
104
- # Try to parse numeric values
105
- if value_str.isdigit():
106
- data[key] = int(value_str)
107
- elif key in ["width", "height"] and value_str.startswith("(int)"):
108
- data[key] = int(value_str[5:].strip())
109
- else:
110
- data[key] = value_str
111
-
112
- return cls.from_dict(data)
157
+ @staticmethod
158
+ def _map_gstreamer_format_to_numpy(
159
+ format: str,
160
+ ) -> tuple[type[np.uint8] | type[np.uint16], int, int]:
161
+ """
162
+ Map GStreamer format strings to numpy dtype.
163
+ Reference: https://gstreamer.freedesktop.org/documentation/video/video-format.html
113
164
 
114
- def to_dict(self) -> dict[str, Any]:
115
- """Convert to dictionary representation."""
116
- result: dict[str, Any] = {}
117
-
118
- if self.format is not None:
119
- result["format"] = self.format
120
- if self.width is not None:
121
- result["width"] = self.width
122
- if self.height is not None:
123
- result["height"] = self.height
124
- if self.framerate is not None:
125
- result["framerate"] = self.framerate
126
- if self.pixel_aspect_ratio is not None:
127
- result["pixel-aspect-ratio"] = self.pixel_aspect_ratio
128
- if self.interlace_mode is not None:
129
- result["interlace-mode"] = self.interlace_mode
130
- if self.colorimetry is not None:
131
- result["colorimetry"] = self.colorimetry
132
- if self.chroma_site is not None:
133
- result["chroma-site"] = self.chroma_site
165
+ Args:
166
+ format: GStreamer format string
134
167
 
135
- # Add extra fields
136
- result.update(self.extra_fields)
168
+ Returns:
169
+ Tuple of (numpy dtype, channels, bytes_per_pixel)
170
+ """
171
+ format_upper = format.upper() if format else "RGB"
172
+
173
+ format_map = {
174
+ # RGB formats
175
+ "RGB": (np.uint8, 3, 3),
176
+ "BGR": (np.uint8, 3, 3),
177
+ "RGBA": (np.uint8, 4, 4),
178
+ "BGRA": (np.uint8, 4, 4),
179
+ "ARGB": (np.uint8, 4, 4),
180
+ "ABGR": (np.uint8, 4, 4),
181
+ "RGBX": (np.uint8, 4, 4), # RGB with padding
182
+ "BGRX": (np.uint8, 4, 4), # BGR with padding
183
+ "XRGB": (np.uint8, 4, 4), # RGB with padding
184
+ "XBGR": (np.uint8, 4, 4), # BGR with padding
185
+ # 16-bit RGB formats
186
+ "RGB16": (np.uint16, 3, 6),
187
+ "BGR16": (np.uint16, 3, 6),
188
+ # Grayscale formats
189
+ "GRAY8": (np.uint8, 1, 1),
190
+ "GRAY16_LE": (np.uint16, 1, 2),
191
+ "GRAY16_BE": (np.uint16, 1, 2),
192
+ # YUV planar formats (Y plane only for simplicity)
193
+ "I420": (np.uint8, 1, 1),
194
+ "YV12": (np.uint8, 1, 1),
195
+ "NV12": (np.uint8, 1, 1),
196
+ "NV21": (np.uint8, 1, 1),
197
+ # YUV packed formats
198
+ "YUY2": (np.uint8, 2, 2),
199
+ "UYVY": (np.uint8, 2, 2),
200
+ "YVYU": (np.uint8, 2, 2),
201
+ # Bayer formats (raw sensor data)
202
+ "BGGR": (np.uint8, 1, 1),
203
+ "RGGB": (np.uint8, 1, 1),
204
+ "GRBG": (np.uint8, 1, 1),
205
+ "GBRG": (np.uint8, 1, 1),
206
+ }
137
207
 
138
- return result
208
+ # Default to RGB if unknown
209
+ return format_map.get(format_upper, (np.uint8, 3, 3))
139
210
 
140
- def to_string(self) -> str:
141
- """Convert to GStreamer caps string format."""
142
- params = []
143
-
144
- if self.format:
145
- params.append(f"format={self.format}")
146
- if self.width is not None:
147
- params.append(f"width={self.width}")
148
- if self.height is not None:
149
- params.append(f"height={self.height}")
150
- if self.framerate:
151
- params.append(f"framerate={self.framerate}")
152
- if self.pixel_aspect_ratio:
153
- params.append(f"pixel-aspect-ratio={self.pixel_aspect_ratio}")
154
- if self.interlace_mode:
155
- params.append(f"interlace-mode={self.interlace_mode}")
211
+ def create_array(
212
+ self, data: bytes | memoryview | npt.NDArray[np.uint8] | npt.NDArray[np.uint16]
213
+ ) -> npt.NDArray[np.uint8] | npt.NDArray[np.uint16]:
214
+ """
215
+ Create numpy array with proper format from data.
216
+
217
+ Args:
218
+ data: Frame data as bytes, memoryview, or existing numpy array
219
+
220
+ Returns:
221
+ Numpy array with proper shape and dtype
222
+
223
+ Raises:
224
+ ValueError: If data size doesn't match expected frame size
225
+ """
226
+ # Convert memoryview to bytes if needed
227
+ if isinstance(data, memoryview):
228
+ data = bytes(data)
229
+
230
+ # If it's already a numpy array, check size and reshape if needed
231
+ if isinstance(data, np.ndarray):
232
+ if data.size * data.itemsize != self.frame_size:
233
+ raise ValueError(
234
+ f"Data size mismatch. Expected {self.frame_size} bytes for "
235
+ f"{self.width}x{self.height} {self.format}, got {data.size * data.itemsize}"
236
+ )
237
+ # Reshape if needed
238
+ if self.channels == 1:
239
+ return data.reshape((self.height, self.width))
240
+ else:
241
+ return data.reshape((self.height, self.width, self.channels))
242
+
243
+ # Check data size
244
+ if len(data) != self.frame_size:
245
+ raise ValueError(
246
+ f"Data size mismatch. Expected {self.frame_size} bytes for "
247
+ f"{self.width}x{self.height} {self.format}, got {len(data)}"
248
+ )
249
+
250
+ # Create array from bytes
251
+ arr = np.frombuffer(data, dtype=self.depth_type)
252
+
253
+ # Reshape based on channels
254
+ if self.channels == 1:
255
+ return arr.reshape((self.height, self.width))
256
+ else:
257
+ # For multi-channel images, reshape to (height, width, channels)
258
+ total_pixels = self.width * self.height * self.channels
259
+ if self.depth_type == np.uint16:
260
+ # For 16-bit formats, we need to account for the item size
261
+ arr = arr[:total_pixels]
262
+ return arr.reshape((self.height, self.width, self.channels))
263
+
264
+ def create_array_from_pointer(
265
+ self, ptr: int, copy: bool = False
266
+ ) -> npt.NDArray[np.uint8] | npt.NDArray[np.uint16]:
267
+ """
268
+ Create numpy array from memory pointer (zero-copy by default).
156
269
 
157
- # Add extra fields
158
- for key, value in self.extra_fields.items():
159
- params.append(f"{key}={value}")
270
+ Args:
271
+ ptr: Memory pointer as integer
272
+ copy: If True, make a copy of the data; if False, create a view
160
273
 
161
- if params:
162
- return "video/x-raw," + ",".join(params)
274
+ Returns:
275
+ Numpy array with proper shape and dtype
276
+ """
277
+ # Calculate total elements based on depth type
278
+ if self.depth_type == np.uint16:
279
+ total_elements = self.width * self.height * self.channels
163
280
  else:
164
- return "video/x-raw"
281
+ total_elements = self.frame_size
165
282
 
166
- @property
167
- def framerate_tuple(self) -> tuple[int, int] | None:
168
- """Get framerate as a tuple of (numerator, denominator)."""
169
- if not self.framerate or "/" not in self.framerate:
170
- return None
283
+ # Create array from pointer using ctypes
284
+ import ctypes
171
285
 
172
- try:
173
- num, denom = self.framerate.split("/")
174
- return (int(num), int(denom))
175
- except (ValueError, AttributeError):
176
- return None
286
+ # Create a buffer from the pointer
287
+ buffer_size = total_elements * self.depth_type.itemsize
288
+ c_buffer = (ctypes.c_byte * buffer_size).from_address(ptr)
289
+ arr = np.frombuffer(c_buffer, dtype=self.depth_type)
177
290
 
178
- @property
179
- def fps(self) -> float | None:
180
- """Get framerate as floating-point FPS value."""
181
- fr = self.framerate_tuple
182
- if fr and fr[1] != 0:
183
- return fr[0] / fr[1]
184
- return None
291
+ # Reshape based on channels
292
+ if self.channels == 1:
293
+ shaped = arr.reshape((self.height, self.width))
294
+ else:
295
+ shaped = arr.reshape((self.height, self.width, self.channels))
296
+
297
+ return shaped.copy() if copy else shaped
298
+
299
+ def __str__(self) -> str:
300
+ """String representation."""
301
+ # If we have the original caps string, return it for perfect round-tripping
302
+ if self.caps_string:
303
+ return self.caps_string
304
+
305
+ # Otherwise build a simple display string
306
+ fps = f" @ {self.framerate:.2f}fps" if self.framerate else ""
307
+ return f"{self.width}x{self.height} {self.format}{fps}"
308
+
309
+ def to_dict(self) -> dict[str, Any]:
310
+ """Convert to dictionary representation."""
311
+ result = {
312
+ "width": self.width,
313
+ "height": self.height,
314
+ "format": self.format,
315
+ "channels": self.channels,
316
+ "bytes_per_pixel": self.bytes_per_pixel,
317
+ }
318
+
319
+ if self.framerate_num is not None:
320
+ result["framerate_num"] = self.framerate_num
321
+ if self.framerate_den is not None:
322
+ result["framerate_den"] = self.framerate_den
323
+ if self.interlace_mode:
324
+ result["interlace_mode"] = self.interlace_mode
325
+ if self.colorimetry:
326
+ result["colorimetry"] = self.colorimetry
327
+
328
+ return result
185
329
 
186
330
 
187
331
  @dataclass
@@ -190,6 +334,7 @@ class GstMetadata:
190
334
  GStreamer metadata structure.
191
335
 
192
336
  Matches the JSON structure written by GStreamer plugins.
337
+ Compatible with C# GstMetadata record.
193
338
  """
194
339
 
195
340
  type: str
@@ -207,24 +352,46 @@ class GstMetadata:
207
352
 
208
353
  Returns:
209
354
  GstMetadata instance
355
+
356
+ Raises:
357
+ ValueError: If JSON is invalid or missing required fields
210
358
  """
211
- data = json.loads(json_data) if isinstance(json_data, (str, bytes)) else json_data
359
+ # Parse JSON if needed
360
+ if isinstance(json_data, (str, bytes)):
361
+ if isinstance(json_data, bytes):
362
+ json_data = json_data.decode("utf-8")
363
+ try:
364
+ data = json.loads(json_data)
365
+ except json.JSONDecodeError as e:
366
+ raise ValueError(f"Invalid JSON: {e}") from e
367
+ else:
368
+ data = json_data
369
+
370
+ # Validate required fields
371
+ if not isinstance(data, dict):
372
+ raise ValueError("JSON must be an object/dictionary")
373
+
374
+ # Get required fields
375
+ type_str = data.get("type", "")
376
+ version = data.get("version", "")
377
+ element_name = data.get("element_name", "")
212
378
 
213
- # Parse caps
214
- caps_data = data.get("caps", {})
379
+ # Parse caps - it's a STRING in the JSON!
380
+ caps_data = data.get("caps")
215
381
  if isinstance(caps_data, str):
216
- caps = GstCaps.from_string(caps_data)
382
+ # This is the normal case - caps is a string that needs parsing
383
+ caps = GstCaps.parse(caps_data)
217
384
  elif isinstance(caps_data, dict):
218
- caps = GstCaps.from_dict(caps_data)
385
+ # Fallback for dict format (shouldn't happen with real GStreamer)
386
+ # Create a simple caps from dict
387
+ width = caps_data.get("width", 640)
388
+ height = caps_data.get("height", 480)
389
+ format_str = caps_data.get("format", "RGB")
390
+ caps = GstCaps.from_simple(width, height, format_str)
219
391
  else:
220
- caps = GstCaps()
392
+ raise ValueError(f"Invalid caps data type: {type(caps_data)}")
221
393
 
222
- return cls(
223
- type=data.get("type", ""),
224
- version=data.get("version", ""),
225
- caps=caps,
226
- element_name=data.get("element_name", ""),
227
- )
394
+ return cls(type=type_str, version=version, caps=caps, element_name=element_name)
228
395
 
229
396
  def to_json(self) -> str:
230
397
  """Convert to JSON string."""
@@ -235,6 +402,10 @@ class GstMetadata:
235
402
  return {
236
403
  "type": self.type,
237
404
  "version": self.version,
238
- "caps": self.caps.to_dict(),
405
+ "caps": str(self.caps), # Caps as string for C# compatibility
239
406
  "element_name": self.element_name,
240
407
  }
408
+
409
+ def __str__(self) -> str:
410
+ """String representation."""
411
+ return f"GstMetadata(type={self.type}, element={self.element_name}, caps={self.caps})"
@@ -20,6 +20,9 @@ if TYPE_CHECKING:
20
20
  # Type alias for OpenCV Mat
21
21
  Mat = np.ndarray[Any, Any]
22
22
 
23
+ # Module logger
24
+ logger = logging.getLogger(__name__)
25
+
23
26
 
24
27
  class RocketWelderClient:
25
28
  """
@@ -28,20 +31,18 @@ class RocketWelderClient:
28
31
  Provides a unified interface for different connection types and protocols.
29
32
  """
30
33
 
31
- def __init__(self, connection: str | ConnectionString, logger: logging.Logger | None = None):
34
+ def __init__(self, connection: str | ConnectionString):
32
35
  """
33
36
  Initialize the RocketWelder client.
34
37
 
35
38
  Args:
36
39
  connection: Connection string or ConnectionString object
37
- logger: Optional logger instance
38
40
  """
39
41
  if isinstance(connection, str):
40
42
  self._connection = ConnectionString.parse(connection)
41
43
  else:
42
44
  self._connection = connection
43
45
 
44
- self._logger = logger or logging.getLogger(__name__)
45
46
  self._controller: IController | None = None
46
47
  self._lock = threading.Lock()
47
48
 
@@ -93,15 +94,15 @@ class RocketWelderClient:
93
94
  # Create appropriate controller based on connection
94
95
  if self._connection.protocol == Protocol.SHM:
95
96
  if self._connection.connection_mode == ConnectionMode.DUPLEX:
96
- self._controller = DuplexShmController(self._connection, self._logger)
97
+ self._controller = DuplexShmController(self._connection)
97
98
  else:
98
- self._controller = OneWayShmController(self._connection, self._logger)
99
+ self._controller = OneWayShmController(self._connection)
99
100
  else:
100
101
  raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
101
102
 
102
103
  # Start the controller
103
104
  self._controller.start(on_frame, cancellation_token) # type: ignore[arg-type]
104
- self._logger.info("RocketWelder client started with %s", self._connection)
105
+ logger.info("RocketWelder client started with %s", self._connection)
105
106
 
106
107
  def stop(self) -> None:
107
108
  """Stop the client and clean up resources."""
@@ -109,7 +110,7 @@ class RocketWelderClient:
109
110
  if self._controller:
110
111
  self._controller.stop()
111
112
  self._controller = None
112
- self._logger.info("RocketWelder client stopped")
113
+ logger.info("RocketWelder client stopped")
113
114
 
114
115
  def __enter__(self) -> RocketWelderClient:
115
116
  """Context manager entry."""
@@ -125,7 +126,6 @@ class RocketWelderClient:
125
126
  buffer_name: str,
126
127
  buffer_size: str = "256MB",
127
128
  metadata_size: str = "4KB",
128
- logger: logging.Logger | None = None,
129
129
  ) -> RocketWelderClient:
130
130
  """
131
131
  Create a one-way shared memory client.
@@ -134,7 +134,6 @@ class RocketWelderClient:
134
134
  buffer_name: Name of the shared memory buffer
135
135
  buffer_size: Size of the buffer (e.g., "256MB")
136
136
  metadata_size: Size of metadata buffer (e.g., "4KB")
137
- logger: Optional logger
138
137
 
139
138
  Returns:
140
139
  Configured RocketWelderClient instance
@@ -142,7 +141,7 @@ class RocketWelderClient:
142
141
  connection_str = (
143
142
  f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=OneWay"
144
143
  )
145
- return cls(connection_str, logger)
144
+ return cls(connection_str)
146
145
 
147
146
  @classmethod
148
147
  def create_duplex_shm(
@@ -150,7 +149,6 @@ class RocketWelderClient:
150
149
  buffer_name: str,
151
150
  buffer_size: str = "256MB",
152
151
  metadata_size: str = "4KB",
153
- logger: logging.Logger | None = None,
154
152
  ) -> RocketWelderClient:
155
153
  """
156
154
  Create a duplex shared memory client.
@@ -159,7 +157,6 @@ class RocketWelderClient:
159
157
  buffer_name: Name of the shared memory buffer
160
158
  buffer_size: Size of the buffer (e.g., "256MB")
161
159
  metadata_size: Size of metadata buffer (e.g., "4KB")
162
- logger: Optional logger
163
160
 
164
161
  Returns:
165
162
  Configured RocketWelderClient instance
@@ -167,4 +164,4 @@ class RocketWelderClient:
167
164
  connection_str = (
168
165
  f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=Duplex"
169
166
  )
170
- return cls(connection_str, logger)
167
+ return cls(connection_str)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocket-welder-sdk
3
- Version: 1.1.0
3
+ Version: 1.1.25
4
4
  Summary: High-performance video streaming SDK for RocketWelder services using ZeroBuffer IPC
5
5
  Home-page: https://github.com/modelingevolution/rocket-welder-sdk
6
6
  Author: ModelingEvolution
@@ -27,7 +27,8 @@ Requires-Python: >=3.8
27
27
  Description-Content-Type: text/markdown
28
28
  Requires-Dist: numpy>=1.20.0
29
29
  Requires-Dist: opencv-python>=4.5.0
30
- Requires-Dist: zerobuffer-ipc>=1.1.10
30
+ Requires-Dist: zerobuffer-ipc>=1.1.17
31
+ Requires-Dist: pydantic>=2.5.0
31
32
  Provides-Extra: dev
32
33
  Requires-Dist: pytest>=7.0; extra == "dev"
33
34
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -0,0 +1,11 @@
1
+ rocket_welder_sdk/__init__.py,sha256=hv0U_UsvIjyLhZLsumjGwKrBJs8vzb1kczUzANilHug,1789
2
+ rocket_welder_sdk/bytes_size.py,sha256=Myl29-wyWCIYdbMmgaxXebT8Dz8_Fwcr3fnfaNW81P0,7463
3
+ rocket_welder_sdk/connection_string.py,sha256=msDgHD7525UXvrNPqKGpYgQJJtmOfS_XEnX3JA8LIz0,7731
4
+ rocket_welder_sdk/controllers.py,sha256=IhPJfwVlk0pxOuTRPW_Xp_3H_BZG6AJXQfS7DCfBrhg,26177
5
+ rocket_welder_sdk/gst_metadata.py,sha256=jEQvZX4BdR6OR3lqp12PV-HEXZhcxfiS010diA2CbMM,14213
6
+ rocket_welder_sdk/py.typed,sha256=0cXFZXmes4Y-vnl4lO3HtyyyWaFNw85B7tJdFeCtHDc,67
7
+ rocket_welder_sdk/rocket_welder_client.py,sha256=uvaAtog-xVCY9YldisgkCnCkbEWMHEgrJUUWJA1J3hg,5237
8
+ rocket_welder_sdk-1.1.25.dist-info/METADATA,sha256=4FF40eEwgdMu0vxF0cwsHgdayWGHc7xQ-C9WJ0KY6To,14362
9
+ rocket_welder_sdk-1.1.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ rocket_welder_sdk-1.1.25.dist-info/top_level.txt,sha256=2iZvBjnwVCUW-uDE23-eJld5PZ9-mlPI69QiXM5IrTA,18
11
+ rocket_welder_sdk-1.1.25.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- rocket_welder_sdk/__init__.py,sha256=6lQUKg8w-qK_5DRFOiydCdirGAj9t-dcCBmDyuW9GNc,909
2
- rocket_welder_sdk/bytes_size.py,sha256=Myl29-wyWCIYdbMmgaxXebT8Dz8_Fwcr3fnfaNW81P0,7463
3
- rocket_welder_sdk/connection_string.py,sha256=msDgHD7525UXvrNPqKGpYgQJJtmOfS_XEnX3JA8LIz0,7731
4
- rocket_welder_sdk/controllers.py,sha256=acrGGBrFvHO2NUIwaj1wsX7bef0Pn3bgYJlcJJUn3bE,24032
5
- rocket_welder_sdk/gst_metadata.py,sha256=K0FByW-BiRPPmveTPLKl1G3A_f9Tog0i8wukiaaXnig,7201
6
- rocket_welder_sdk/py.typed,sha256=0cXFZXmes4Y-vnl4lO3HtyyyWaFNw85B7tJdFeCtHDc,67
7
- rocket_welder_sdk/rocket_welder_client.py,sha256=1-W1ofdMof9njmLOx4eV8vgxtU2xgk96h8V01Yey9wE,5547
8
- rocket_welder_sdk-1.1.0.dist-info/METADATA,sha256=3g0N-hAnSmhEddO1fl8ox3KkiRL9V6gyOL90Zqr9JAg,14330
9
- rocket_welder_sdk-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- rocket_welder_sdk-1.1.0.dist-info/top_level.txt,sha256=2iZvBjnwVCUW-uDE23-eJld5PZ9-mlPI69QiXM5IrTA,18
11
- rocket_welder_sdk-1.1.0.dist-info/RECORD,,