shmx


Nameshmx JSON
Version 1.0.1 PyPI version JSON
download
home_pageNone
SummaryHigh-performance shared-memory IPC for frame streaming
upload_time2025-10-13 04:31:52
maintainerNone
docs_urlNone
authorSHMX Contributors
requires_python>=3.7
licenseMIT
keywords ipc shared-memory high-performance streaming frame
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # SHMX - High-Performance Shared Memory IPC

[![PyPI version](https://badge.fury.io/py/shmx.svg)](https://badge.fury.io/py/shmx)
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)

A **lock-free, zero-copy** shared-memory IPC library for high-performance frame streaming between processes. SHMX enables ultra-low latency data exchange with typed schemas, per-frame metadata, and backpressure tolerance.

## πŸš€ Features

- **Zero-Copy Data Transfer** - Direct memory access via shared memory with Python `memoryview` support
- **Lock-Free Architecture** - Wait-free reads, minimal contention for writes
- **Typed Schema System** - Self-describing streams with static metadata directory
- **Backpressure Tolerant** - Slow readers drop frames gracefully without blocking the producer
- **Bidirectional Control** - Per-reader control rings for client→server messages
- **Cross-Platform** - Windows and Linux support
- **Pure Python Interface** - No numpy required (but compatible), returns native Python types
- **Introspection Tools** - Built-in inspector for debugging and monitoring

## πŸ“¦ Installation

```bash
pip install shmx
```

## πŸƒ Quick Start

### Basic Producer-Consumer Example

**Producer (Server):**

```python
import shmx
import time
import struct

# Define stream schema
streams = [
    shmx.create_stream_spec(
        stream_id=1,
        name="frame_id",
        dtype_code=shmx.DT_U64,
        components=1,
        bytes_per_elem=8
    ),
    shmx.create_stream_spec(
        stream_id=2,
        name="timestamp",
        dtype_code=shmx.DT_F64,
        components=1,
        bytes_per_elem=8
    ),
    shmx.create_stream_spec(
        stream_id=3,
        name="data",
        dtype_code=shmx.DT_F32,
        components=1,
        bytes_per_elem=4
    ),
]

# Create server
server = shmx.Server()
if not server.create(
    name="my_stream",
    slots=4,
    reader_slots=16,
    static_bytes_cap=4096,
    frame_bytes_cap=65536,
    control_per_reader=4096,
    streams=streams
):
    print("Failed to create server")
    exit(1)

print(f"Server created: {server.get_header_info()}")

# Publish frames
for i in range(100):
    frame = server.begin_frame()
    
    # Append streams
    server.append_stream(frame, 1, struct.pack('Q', i), 1)
    server.append_stream(frame, 2, struct.pack('d', time.time()), 1)
    server.append_stream(frame, 3, struct.pack('10f', *range(10)), 10)
    
    # Publish
    server.publish_frame(frame, time.time())
    time.sleep(0.01)

server.destroy()
```

**Consumer (Client):**

```python
import shmx
import time

# Open client
client = shmx.Client()
if not client.open("my_stream"):
    print("Failed to open client")
    exit(1)

print(f"Client connected: {client.get_header_info()}")
print(f"Available streams: {client.get_streams_info()}")

# Read frames
for _ in range(100):
    frame = client.get_latest_frame()
    if frame is not None:
        # Access metadata
        metadata = frame['__metadata__']
        print(f"Frame {metadata['frame_id']}: sim_time={metadata['sim_time']:.3f}")
        
        # Access stream data (zero-copy memoryview)
        frame_id_data = frame['frame_id']['data']
        timestamp_data = frame['timestamp']['data']
        data_stream = frame['data']['data']
        
        # Convert to native types if needed
        import struct
        frame_id = struct.unpack('Q', frame_id_data)[0]
        timestamp = struct.unpack('d', timestamp_data)[0]
        print(f"  frame_id={frame_id}, timestamp={timestamp:.3f}")
    
    time.sleep(0.01)

client.close()
```

## πŸ“š API Documentation

### Server Class

The `Server` class publishes frames to shared memory for multiple clients to consume.

#### `Server()`

Create a new server instance.

#### `create(name, slots=3, reader_slots=16, static_bytes_cap=4096, frame_bytes_cap=65536, control_per_reader=4096, streams=[]) -> bool`

Create and initialize the shared memory region.

**Parameters:**
- `name` (str): Shared memory region name (acts as the channel identifier)
- `slots` (int): Number of frame slots in the ring buffer (default: 3)
- `reader_slots` (int): Maximum number of concurrent readers (default: 16)
- `static_bytes_cap` (int): Capacity for static metadata (default: 4096)
- `frame_bytes_cap` (int): Maximum bytes per frame payload (default: 65536)
- `control_per_reader` (int): Control ring buffer size per reader (default: 4096)
- `streams` (list): List of stream specification dicts (see `create_stream_spec`)

**Returns:** `bool` - True if successful, False otherwise

#### `destroy()`

Destroy and release the shared memory region.

#### `begin_frame() -> frame_handle`

Begin a new frame. Returns an opaque frame handle to be used with `append_stream` and `publish_frame`.

#### `append_stream(frame_handle, stream_id, data, elem_count) -> bool`

Append stream data to the current frame.

**Parameters:**
- `frame_handle`: Frame handle from `begin_frame()`
- `stream_id` (int): Stream ID matching the schema
- `data` (bytes): Raw binary data
- `elem_count` (int): Number of elements in the data

**Returns:** `bool` - True if successful

#### `publish_frame(frame_handle, sim_time) -> bool`

Publish the frame to shared memory, making it available to clients.

**Parameters:**
- `frame_handle`: Frame handle from `begin_frame()`
- `sim_time` (float): Simulation/frame timestamp

**Returns:** `bool` - True if successful

#### `poll_control(max_messages=256) -> list`

Poll control messages from clients.

**Returns:** List of dicts with keys: `reader_id`, `type`, `data` (bytes)

#### `snapshot_readers() -> list`

Get a snapshot of all connected readers.

**Returns:** List of dicts with keys: `reader_id`, `heartbeat`, `last_frame_seen`, `in_use`

#### `reap_stale_readers(now_ticks, timeout_ticks) -> int`

Remove stale readers that haven't sent heartbeats.

**Returns:** Number of readers reaped

#### `get_header_info() -> dict`

Get header/metadata information about the shared memory region.

---

### Client Class

The `Client` class consumes frames from shared memory published by a server.

#### `Client()`

Create a new client instance.

#### `open(name) -> bool`

Open and connect to a shared memory region.

**Parameters:**
- `name` (str): Shared memory region name

**Returns:** `bool` - True if successful

#### `close()`

Close the connection and release resources.

#### `is_open() -> bool`

Check if the client is currently connected.

#### `get_latest_frame() -> dict | None`

Get the most recent frame from the server.

**Returns:**
- `None` if no frame available or validation failed
- `dict` with the following structure:
  ```python
  {
      '__metadata__': {
          'frame_id': int,
          'sim_time': float,
          'payload_bytes': int,
          'tlv_count': int
      },
      'stream_name': {
          'data': memoryview,      # Zero-copy buffer
          'elem_count': int,
          'bytes': int
      },
      # ... additional streams
  }
  ```

**Note:** The `memoryview` objects provide zero-copy access to the shared memory. Data is only valid until the next frame is published. If you need to retain data, convert to bytes or copy it.

#### `get_streams_info() -> list`

Get metadata about all available streams.

**Returns:** List of dicts with keys: `id`, `name`, `dtype`, `dtype_code`, `components`, `layout`, `bytes_per_elem`, and optionally `extra`

#### `get_header_info() -> dict`

Get header/metadata information about the shared memory region.

#### `refresh_static() -> bool`

Refresh the static stream metadata (useful if schema changes).

#### `send_control(type, data) -> bool`

Send a control message to the server.

**Parameters:**
- `type` (int): Message type identifier
- `data` (bytes): Message payload

#### `send_control_empty(type) -> bool`

Send a control message without payload.

---

### Inspector Class

The `Inspector` class provides read-only introspection of shared memory state for debugging and monitoring.

#### `Inspector()`

Create a new inspector instance.

#### `open(name) -> bool`

Open a shared memory region in read-only mode.

#### `close()`

Close the connection.

#### `inspect() -> dict`

Get a comprehensive inspection report.

**Returns:** Dict with keys: `session_id`, `static_gen`, `frame_seq`, `readers_connected`, `streams`, `readers`

#### `get_header_info() -> dict`

Get header information.

#### `get_streams_info() -> list`

Get stream metadata.

#### `get_readers_info() -> list`

Get information about connected readers.

---

### Helper Functions

#### `create_stream_spec(stream_id, name, dtype_code, components, bytes_per_elem, layout_code=None, extra=None) -> dict`

Helper function to create stream specification dictionaries for `Server.create()`.

**Parameters:**
- `stream_id` (int): Unique stream identifier
- `name` (str): Human-readable stream name
- `dtype_code` (int): Data type constant (e.g., `shmx.DT_F32`)
- `components` (int): Number of components per element (1 for scalar)
- `bytes_per_elem` (int): Total bytes per element
- `layout_code` (int, optional): Layout constant (default: `LAYOUT_SOA_SCALAR`)
- `extra` (bytes, optional): Additional metadata

**Returns:** Dict suitable for `Server.create()` streams parameter

#### `dtype_to_string(dtype_code) -> str`

Convert a dtype code to a human-readable string.

#### `layout_to_string(layout_code) -> str`

Convert a layout code to a human-readable string.

---

### Constants

#### Data Types

- `DT_BOOL` - Boolean (1 byte)
- `DT_I8`, `DT_U8` - 8-bit signed/unsigned integer
- `DT_I16`, `DT_U16` - 16-bit signed/unsigned integer
- `DT_I32`, `DT_U32` - 32-bit signed/unsigned integer
- `DT_I64`, `DT_U64` - 64-bit signed/unsigned integer
- `DT_F16` - 16-bit float (half precision)
- `DT_BF16` - 16-bit bfloat
- `DT_F32` - 32-bit float (single precision)
- `DT_F64` - 64-bit float (double precision)

#### Layouts

- `LAYOUT_SOA_SCALAR` - Scalar data (default)
- `LAYOUT_AOS_VECTOR` - Vector data (interleaved)

#### TLV Types

- `TLV_STATIC_DIR` - Static directory entry
- `TLV_FRAME_STREAM` - Frame stream data
- `TLV_CONTROL_USER` - User control message

---

## πŸ“– Complete Examples

### Example 1: Video Frame Streaming

```python
import shmx
import numpy as np
import time

# Server: Publish video frames
def video_server():
    streams = [
        shmx.create_stream_spec(1, "width", shmx.DT_U32, 1, 4),
        shmx.create_stream_spec(2, "height", shmx.DT_U32, 1, 4),
        shmx.create_stream_spec(3, "pixels", shmx.DT_U8, 1, 1),
    ]
    
    server = shmx.Server()
    server.create("video_stream", slots=4, frame_bytes_cap=1920*1080*3, streams=streams)
    
    width, height = 1920, 1080
    
    for frame_num in range(1000):
        # Generate dummy frame (would be real camera data)
        pixels = np.random.randint(0, 255, (height, width, 3), dtype=np.uint8)
        
        frame = server.begin_frame()
        server.append_stream(frame, 1, width.to_bytes(4, 'little'), 1)
        server.append_stream(frame, 2, height.to_bytes(4, 'little'), 1)
        server.append_stream(frame, 3, pixels.tobytes(), width * height * 3)
        server.publish_frame(frame, time.time())
        
        time.sleep(1/30)  # 30 FPS
    
    server.destroy()

# Client: Consume video frames
def video_client():
    client = shmx.Client()
    client.open("video_stream")
    
    while True:
        frame = client.get_latest_frame()
        if frame:
            width = int.from_bytes(frame['width']['data'], 'little')
            height = int.from_bytes(frame['height']['data'], 'little')
            
            # Zero-copy access to pixel data
            pixels_view = frame['pixels']['data']
            
            # Convert to numpy for processing (creates copy)
            pixels = np.frombuffer(pixels_view, dtype=np.uint8).reshape((height, width, 3))
            
            print(f"Received frame {frame['__metadata__']['frame_id']}: {width}x{height}")
            
            # Process frame...
        
        time.sleep(1/30)
```

### Example 2: Sensor Data with Control Messages

```python
import shmx
import struct
import time

# Control message types
CTRL_SET_RATE = 0x1001
CTRL_RESET = 0x1002

# Server with control message handling
def sensor_server():
    streams = [
        shmx.create_stream_spec(1, "temperature", shmx.DT_F32, 1, 4),
        shmx.create_stream_spec(2, "pressure", shmx.DT_F32, 1, 4),
        shmx.create_stream_spec(3, "humidity", shmx.DT_F32, 1, 4),
    ]
    
    server = shmx.Server()
    server.create("sensors", streams=streams)
    
    rate = 10.0  # Hz
    
    for i in range(1000):
        # Poll control messages
        msgs = server.poll_control()
        for msg in msgs:
            if msg['type'] == CTRL_SET_RATE:
                new_rate = struct.unpack('f', msg['data'])[0]
                print(f"Reader {msg['reader_id']} set rate to {new_rate} Hz")
                rate = new_rate
            elif msg['type'] == CTRL_RESET:
                print(f"Reader {msg['reader_id']} requested reset")
        
        # Publish sensor data
        frame = server.begin_frame()
        server.append_stream(frame, 1, struct.pack('f', 25.0 + i * 0.1), 1)
        server.append_stream(frame, 2, struct.pack('f', 1013.25 + i * 0.01), 1)
        server.append_stream(frame, 3, struct.pack('f', 45.0 + i * 0.05), 1)
        server.publish_frame(frame, time.time())
        
        # Check reader health
        readers = server.snapshot_readers()
        print(f"Active readers: {len([r for r in readers if r['in_use']])}")
        
        time.sleep(1.0 / rate)
    
    server.destroy()

# Client with control messages
def sensor_client():
    client = shmx.Client()
    client.open("sensors")
    
    print("Streams:", client.get_streams_info())
    
    # Request faster rate
    client.send_control(CTRL_SET_RATE, struct.pack('f', 100.0))
    
    for _ in range(100):
        frame = client.get_latest_frame()
        if frame:
            temp = struct.unpack('f', frame['temperature']['data'])[0]
            pressure = struct.unpack('f', frame['pressure']['data'])[0]
            humidity = struct.unpack('f', frame['humidity']['data'])[0]
            
            print(f"T={temp:.1f}Β°C P={pressure:.2f}hPa H={humidity:.1f}%")
        
        time.sleep(0.01)
    
    client.close()
```

### Example 3: Multiple Streams with Numpy

```python
import shmx
import numpy as np
import time

def numpy_example():
    # Server
    streams = [
        shmx.create_stream_spec(1, "positions", shmx.DT_F32, 3, 12),
        shmx.create_stream_spec(2, "velocities", shmx.DT_F32, 3, 12),
        shmx.create_stream_spec(3, "ids", shmx.DT_U32, 1, 4),
    ]
    
    server = shmx.Server()
    server.create("particles", frame_bytes_cap=1024*1024, streams=streams)
    
    # Client
    client = shmx.Client()
    client.open("particles")
    
    # Publish
    num_particles = 1000
    for i in range(100):
        positions = np.random.randn(num_particles, 3).astype(np.float32)
        velocities = np.random.randn(num_particles, 3).astype(np.float32)
        ids = np.arange(num_particles, dtype=np.uint32)
        
        frame = server.begin_frame()
        server.append_stream(frame, 1, positions.tobytes(), num_particles)
        server.append_stream(frame, 2, velocities.tobytes(), num_particles)
        server.append_stream(frame, 3, ids.tobytes(), num_particles)
        server.publish_frame(frame, i * 0.01)
        
        # Read back
        frame_data = client.get_latest_frame()
        if frame_data:
            # Zero-copy view into shared memory
            pos_view = np.frombuffer(frame_data['positions']['data'], dtype=np.float32)
            pos_array = pos_view.reshape(-1, 3)
            
            vel_view = np.frombuffer(frame_data['velocities']['data'], dtype=np.float32)
            vel_array = vel_view.reshape(-1, 3)
            
            print(f"Frame {i}: {len(pos_array)} particles")
            print(f"  Position range: [{pos_array.min():.2f}, {pos_array.max():.2f}]")
        
        time.sleep(0.01)
    
    server.destroy()
    client.close()

if __name__ == '__main__':
    numpy_example()
```

### Example 4: Inspector for Debugging

```python
import shmx
import time

def inspect_stream(name):
    """Inspect a running stream without interfering"""
    inspector = shmx.Inspector()
    
    if not inspector.open(name):
        print(f"Failed to open stream '{name}'")
        return
    
    # Get complete report
    report = inspector.inspect()
    
    print(f"\n=== Stream Inspection: {name} ===")
    print(f"Session ID: {report['session_id']}")
    print(f"Frame Sequence: {report['frame_seq']}")
    print(f"Readers Connected: {report['readers_connected']}")
    print(f"Static Generation: {report['static_gen']}")
    
    print("\nStreams:")
    for stream in report['streams']:
        print(f"  [{stream['id']}] {stream['name']}")
        print(f"      Type: {stream['dtype']} x {stream['components']}")
        print(f"      Layout: {stream['layout']}")
        print(f"      Bytes/elem: {stream['bytes_per_elem']}")
    
    print("\nReaders:")
    for reader in report['readers']:
        if reader['in_use']:
            print(f"  Reader {reader['reader_id']}")
            print(f"    Last frame: {reader['last_frame_seen']}")
            print(f"    Heartbeat: {reader['heartbeat']}")
    
    inspector.close()

# Usage
if __name__ == '__main__':
    inspect_stream("my_stream")
```

### Example 5: Multi-Process Communication

```python
import shmx
import multiprocessing
import time
import struct

def producer_process(name):
    """Producer process"""
    streams = [
        shmx.create_stream_spec(1, "counter", shmx.DT_U64, 1, 8),
        shmx.create_stream_spec(2, "value", shmx.DT_F64, 1, 8),
    ]
    
    server = shmx.Server()
    server.create(name, streams=streams)
    print(f"Producer: Started on '{name}'")
    
    for i in range(100):
        frame = server.begin_frame()
        server.append_stream(frame, 1, struct.pack('Q', i), 1)
        server.append_stream(frame, 2, struct.pack('d', i * 3.14), 1)
        server.publish_frame(frame, time.time())
        time.sleep(0.1)
    
    server.destroy()
    print("Producer: Done")

def consumer_process(name, consumer_id):
    """Consumer process"""
    time.sleep(0.5)  # Wait for producer
    
    client = shmx.Client()
    if not client.open(name):
        print(f"Consumer {consumer_id}: Failed to connect")
        return
    
    print(f"Consumer {consumer_id}: Connected to '{name}'")
    
    count = 0
    for _ in range(50):
        frame = client.get_latest_frame()
        if frame:
            counter = struct.unpack('Q', frame['counter']['data'])[0]
            value = struct.unpack('d', frame['value']['data'])[0]
            print(f"Consumer {consumer_id}: counter={counter}, value={value:.2f}")
            count += 1
        time.sleep(0.2)
    
    client.close()
    print(f"Consumer {consumer_id}: Received {count} frames")

if __name__ == '__main__':
    stream_name = "multi_process_test"
    
    # Start producer
    producer = multiprocessing.Process(target=producer_process, args=(stream_name,))
    producer.start()
    
    # Start multiple consumers
    consumers = []
    for i in range(3):
        consumer = multiprocessing.Process(target=consumer_process, args=(stream_name, i))
        consumer.start()
        consumers.append(consumer)
    
    # Wait for completion
    producer.join()
    for consumer in consumers:
        consumer.join()
    
    print("All processes completed")
```

## πŸ”§ Advanced Usage

### Memory Layout and Performance

SHMX uses a ring buffer architecture with the following characteristics:

- **Slots**: Number of frames buffered (configure with `slots` parameter)
- **Frame Size**: Maximum frame payload size (`frame_bytes_cap`)
- **Zero-Copy**: Client `memoryview` objects point directly to shared memory
- **Lock-Free**: Atomic operations for synchronization

**Performance Tips:**
1. Allocate `frame_bytes_cap` based on your maximum frame size
2. Use `slots=3` or `slots=4` for typical applications
3. `memoryview` data is only valid until the next server frame publication
4. Convert to `bytes()` or `bytearray()` if you need to retain data

### Error Handling

```python
import shmx

# Always check return values
server = shmx.Server()
if not server.create("my_stream", ...):
    print("Failed to create server - check permissions and naming")
    exit(1)

client = shmx.Client()
if not client.open("my_stream"):
    print("Failed to open client - server may not be running")
    exit(1)

# Check for None when reading frames
frame = client.get_latest_frame()
if frame is None:
    print("No frame available or checksum mismatch")
```

### Schema Evolution

```python
# Server can update static metadata
server.write_static_append(b"extra_metadata", len(b"extra_metadata"))

# Clients can refresh
client.refresh_static()
streams = client.get_streams_info()
```

## πŸ› Troubleshooting

### Common Issues

1. **"Failed to create server"**
   - Check if name is already in use
   - Ensure sufficient permissions
   - On Linux: Check `/dev/shm/` permissions

2. **"Failed to open client"**
   - Verify server is running
   - Check that names match exactly
   - Ensure client has read permissions

3. **Frames are None**
   - Server may not be publishing yet
   - Check frame checksums (possible corruption)
   - Verify session IDs match

4. **Memory issues**
   - Increase `frame_bytes_cap` if frames are too large
   - Check total shared memory usage
   - On Linux: `df -h /dev/shm`

## πŸ“Š Performance Characteristics

- **Latency**: < 1 microsecond for local reads (CPU cache hit)
- **Throughput**: Limited by memory bandwidth (10+ GB/s typical)
- **Scalability**: Supports 16+ concurrent readers (configurable)
- **Overhead**: Minimal - atomic operations only

## πŸ”— Related Projects

- Main repository: [github.com/HinaPE/shared-mem-ipc](https://github.com/HinaPE/shared-mem-ipc)
- C++ header-only library included

## πŸ“„ License

Mozilla Public License Version 2.0

## 🀝 Contributing

Contributions are welcome! Please visit the main repository for guidelines.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "shmx",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": null,
    "keywords": "ipc, shared-memory, high-performance, streaming, frame",
    "author": "SHMX Contributors",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/90/07/2bb32a2c96a4c376de013b379647e44c208857c28d0b87b3cca95b5b7f33/shmx-1.0.1.tar.gz",
    "platform": null,
    "description": "# SHMX - High-Performance Shared Memory IPC\n\n[![PyPI version](https://badge.fury.io/py/shmx.svg)](https://badge.fury.io/py/shmx)\n[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)\n\nA **lock-free, zero-copy** shared-memory IPC library for high-performance frame streaming between processes. SHMX enables ultra-low latency data exchange with typed schemas, per-frame metadata, and backpressure tolerance.\n\n## \ud83d\ude80 Features\n\n- **Zero-Copy Data Transfer** - Direct memory access via shared memory with Python `memoryview` support\n- **Lock-Free Architecture** - Wait-free reads, minimal contention for writes\n- **Typed Schema System** - Self-describing streams with static metadata directory\n- **Backpressure Tolerant** - Slow readers drop frames gracefully without blocking the producer\n- **Bidirectional Control** - Per-reader control rings for client\u2192server messages\n- **Cross-Platform** - Windows and Linux support\n- **Pure Python Interface** - No numpy required (but compatible), returns native Python types\n- **Introspection Tools** - Built-in inspector for debugging and monitoring\n\n## \ud83d\udce6 Installation\n\n```bash\npip install shmx\n```\n\n## \ud83c\udfc3 Quick Start\n\n### Basic Producer-Consumer Example\n\n**Producer (Server):**\n\n```python\nimport shmx\nimport time\nimport struct\n\n# Define stream schema\nstreams = [\n    shmx.create_stream_spec(\n        stream_id=1,\n        name=\"frame_id\",\n        dtype_code=shmx.DT_U64,\n        components=1,\n        bytes_per_elem=8\n    ),\n    shmx.create_stream_spec(\n        stream_id=2,\n        name=\"timestamp\",\n        dtype_code=shmx.DT_F64,\n        components=1,\n        bytes_per_elem=8\n    ),\n    shmx.create_stream_spec(\n        stream_id=3,\n        name=\"data\",\n        dtype_code=shmx.DT_F32,\n        components=1,\n        bytes_per_elem=4\n    ),\n]\n\n# Create server\nserver = shmx.Server()\nif not server.create(\n    name=\"my_stream\",\n    slots=4,\n    reader_slots=16,\n    static_bytes_cap=4096,\n    frame_bytes_cap=65536,\n    control_per_reader=4096,\n    streams=streams\n):\n    print(\"Failed to create server\")\n    exit(1)\n\nprint(f\"Server created: {server.get_header_info()}\")\n\n# Publish frames\nfor i in range(100):\n    frame = server.begin_frame()\n    \n    # Append streams\n    server.append_stream(frame, 1, struct.pack('Q', i), 1)\n    server.append_stream(frame, 2, struct.pack('d', time.time()), 1)\n    server.append_stream(frame, 3, struct.pack('10f', *range(10)), 10)\n    \n    # Publish\n    server.publish_frame(frame, time.time())\n    time.sleep(0.01)\n\nserver.destroy()\n```\n\n**Consumer (Client):**\n\n```python\nimport shmx\nimport time\n\n# Open client\nclient = shmx.Client()\nif not client.open(\"my_stream\"):\n    print(\"Failed to open client\")\n    exit(1)\n\nprint(f\"Client connected: {client.get_header_info()}\")\nprint(f\"Available streams: {client.get_streams_info()}\")\n\n# Read frames\nfor _ in range(100):\n    frame = client.get_latest_frame()\n    if frame is not None:\n        # Access metadata\n        metadata = frame['__metadata__']\n        print(f\"Frame {metadata['frame_id']}: sim_time={metadata['sim_time']:.3f}\")\n        \n        # Access stream data (zero-copy memoryview)\n        frame_id_data = frame['frame_id']['data']\n        timestamp_data = frame['timestamp']['data']\n        data_stream = frame['data']['data']\n        \n        # Convert to native types if needed\n        import struct\n        frame_id = struct.unpack('Q', frame_id_data)[0]\n        timestamp = struct.unpack('d', timestamp_data)[0]\n        print(f\"  frame_id={frame_id}, timestamp={timestamp:.3f}\")\n    \n    time.sleep(0.01)\n\nclient.close()\n```\n\n## \ud83d\udcda API Documentation\n\n### Server Class\n\nThe `Server` class publishes frames to shared memory for multiple clients to consume.\n\n#### `Server()`\n\nCreate a new server instance.\n\n#### `create(name, slots=3, reader_slots=16, static_bytes_cap=4096, frame_bytes_cap=65536, control_per_reader=4096, streams=[]) -> bool`\n\nCreate and initialize the shared memory region.\n\n**Parameters:**\n- `name` (str): Shared memory region name (acts as the channel identifier)\n- `slots` (int): Number of frame slots in the ring buffer (default: 3)\n- `reader_slots` (int): Maximum number of concurrent readers (default: 16)\n- `static_bytes_cap` (int): Capacity for static metadata (default: 4096)\n- `frame_bytes_cap` (int): Maximum bytes per frame payload (default: 65536)\n- `control_per_reader` (int): Control ring buffer size per reader (default: 4096)\n- `streams` (list): List of stream specification dicts (see `create_stream_spec`)\n\n**Returns:** `bool` - True if successful, False otherwise\n\n#### `destroy()`\n\nDestroy and release the shared memory region.\n\n#### `begin_frame() -> frame_handle`\n\nBegin a new frame. Returns an opaque frame handle to be used with `append_stream` and `publish_frame`.\n\n#### `append_stream(frame_handle, stream_id, data, elem_count) -> bool`\n\nAppend stream data to the current frame.\n\n**Parameters:**\n- `frame_handle`: Frame handle from `begin_frame()`\n- `stream_id` (int): Stream ID matching the schema\n- `data` (bytes): Raw binary data\n- `elem_count` (int): Number of elements in the data\n\n**Returns:** `bool` - True if successful\n\n#### `publish_frame(frame_handle, sim_time) -> bool`\n\nPublish the frame to shared memory, making it available to clients.\n\n**Parameters:**\n- `frame_handle`: Frame handle from `begin_frame()`\n- `sim_time` (float): Simulation/frame timestamp\n\n**Returns:** `bool` - True if successful\n\n#### `poll_control(max_messages=256) -> list`\n\nPoll control messages from clients.\n\n**Returns:** List of dicts with keys: `reader_id`, `type`, `data` (bytes)\n\n#### `snapshot_readers() -> list`\n\nGet a snapshot of all connected readers.\n\n**Returns:** List of dicts with keys: `reader_id`, `heartbeat`, `last_frame_seen`, `in_use`\n\n#### `reap_stale_readers(now_ticks, timeout_ticks) -> int`\n\nRemove stale readers that haven't sent heartbeats.\n\n**Returns:** Number of readers reaped\n\n#### `get_header_info() -> dict`\n\nGet header/metadata information about the shared memory region.\n\n---\n\n### Client Class\n\nThe `Client` class consumes frames from shared memory published by a server.\n\n#### `Client()`\n\nCreate a new client instance.\n\n#### `open(name) -> bool`\n\nOpen and connect to a shared memory region.\n\n**Parameters:**\n- `name` (str): Shared memory region name\n\n**Returns:** `bool` - True if successful\n\n#### `close()`\n\nClose the connection and release resources.\n\n#### `is_open() -> bool`\n\nCheck if the client is currently connected.\n\n#### `get_latest_frame() -> dict | None`\n\nGet the most recent frame from the server.\n\n**Returns:**\n- `None` if no frame available or validation failed\n- `dict` with the following structure:\n  ```python\n  {\n      '__metadata__': {\n          'frame_id': int,\n          'sim_time': float,\n          'payload_bytes': int,\n          'tlv_count': int\n      },\n      'stream_name': {\n          'data': memoryview,      # Zero-copy buffer\n          'elem_count': int,\n          'bytes': int\n      },\n      # ... additional streams\n  }\n  ```\n\n**Note:** The `memoryview` objects provide zero-copy access to the shared memory. Data is only valid until the next frame is published. If you need to retain data, convert to bytes or copy it.\n\n#### `get_streams_info() -> list`\n\nGet metadata about all available streams.\n\n**Returns:** List of dicts with keys: `id`, `name`, `dtype`, `dtype_code`, `components`, `layout`, `bytes_per_elem`, and optionally `extra`\n\n#### `get_header_info() -> dict`\n\nGet header/metadata information about the shared memory region.\n\n#### `refresh_static() -> bool`\n\nRefresh the static stream metadata (useful if schema changes).\n\n#### `send_control(type, data) -> bool`\n\nSend a control message to the server.\n\n**Parameters:**\n- `type` (int): Message type identifier\n- `data` (bytes): Message payload\n\n#### `send_control_empty(type) -> bool`\n\nSend a control message without payload.\n\n---\n\n### Inspector Class\n\nThe `Inspector` class provides read-only introspection of shared memory state for debugging and monitoring.\n\n#### `Inspector()`\n\nCreate a new inspector instance.\n\n#### `open(name) -> bool`\n\nOpen a shared memory region in read-only mode.\n\n#### `close()`\n\nClose the connection.\n\n#### `inspect() -> dict`\n\nGet a comprehensive inspection report.\n\n**Returns:** Dict with keys: `session_id`, `static_gen`, `frame_seq`, `readers_connected`, `streams`, `readers`\n\n#### `get_header_info() -> dict`\n\nGet header information.\n\n#### `get_streams_info() -> list`\n\nGet stream metadata.\n\n#### `get_readers_info() -> list`\n\nGet information about connected readers.\n\n---\n\n### Helper Functions\n\n#### `create_stream_spec(stream_id, name, dtype_code, components, bytes_per_elem, layout_code=None, extra=None) -> dict`\n\nHelper function to create stream specification dictionaries for `Server.create()`.\n\n**Parameters:**\n- `stream_id` (int): Unique stream identifier\n- `name` (str): Human-readable stream name\n- `dtype_code` (int): Data type constant (e.g., `shmx.DT_F32`)\n- `components` (int): Number of components per element (1 for scalar)\n- `bytes_per_elem` (int): Total bytes per element\n- `layout_code` (int, optional): Layout constant (default: `LAYOUT_SOA_SCALAR`)\n- `extra` (bytes, optional): Additional metadata\n\n**Returns:** Dict suitable for `Server.create()` streams parameter\n\n#### `dtype_to_string(dtype_code) -> str`\n\nConvert a dtype code to a human-readable string.\n\n#### `layout_to_string(layout_code) -> str`\n\nConvert a layout code to a human-readable string.\n\n---\n\n### Constants\n\n#### Data Types\n\n- `DT_BOOL` - Boolean (1 byte)\n- `DT_I8`, `DT_U8` - 8-bit signed/unsigned integer\n- `DT_I16`, `DT_U16` - 16-bit signed/unsigned integer\n- `DT_I32`, `DT_U32` - 32-bit signed/unsigned integer\n- `DT_I64`, `DT_U64` - 64-bit signed/unsigned integer\n- `DT_F16` - 16-bit float (half precision)\n- `DT_BF16` - 16-bit bfloat\n- `DT_F32` - 32-bit float (single precision)\n- `DT_F64` - 64-bit float (double precision)\n\n#### Layouts\n\n- `LAYOUT_SOA_SCALAR` - Scalar data (default)\n- `LAYOUT_AOS_VECTOR` - Vector data (interleaved)\n\n#### TLV Types\n\n- `TLV_STATIC_DIR` - Static directory entry\n- `TLV_FRAME_STREAM` - Frame stream data\n- `TLV_CONTROL_USER` - User control message\n\n---\n\n## \ud83d\udcd6 Complete Examples\n\n### Example 1: Video Frame Streaming\n\n```python\nimport shmx\nimport numpy as np\nimport time\n\n# Server: Publish video frames\ndef video_server():\n    streams = [\n        shmx.create_stream_spec(1, \"width\", shmx.DT_U32, 1, 4),\n        shmx.create_stream_spec(2, \"height\", shmx.DT_U32, 1, 4),\n        shmx.create_stream_spec(3, \"pixels\", shmx.DT_U8, 1, 1),\n    ]\n    \n    server = shmx.Server()\n    server.create(\"video_stream\", slots=4, frame_bytes_cap=1920*1080*3, streams=streams)\n    \n    width, height = 1920, 1080\n    \n    for frame_num in range(1000):\n        # Generate dummy frame (would be real camera data)\n        pixels = np.random.randint(0, 255, (height, width, 3), dtype=np.uint8)\n        \n        frame = server.begin_frame()\n        server.append_stream(frame, 1, width.to_bytes(4, 'little'), 1)\n        server.append_stream(frame, 2, height.to_bytes(4, 'little'), 1)\n        server.append_stream(frame, 3, pixels.tobytes(), width * height * 3)\n        server.publish_frame(frame, time.time())\n        \n        time.sleep(1/30)  # 30 FPS\n    \n    server.destroy()\n\n# Client: Consume video frames\ndef video_client():\n    client = shmx.Client()\n    client.open(\"video_stream\")\n    \n    while True:\n        frame = client.get_latest_frame()\n        if frame:\n            width = int.from_bytes(frame['width']['data'], 'little')\n            height = int.from_bytes(frame['height']['data'], 'little')\n            \n            # Zero-copy access to pixel data\n            pixels_view = frame['pixels']['data']\n            \n            # Convert to numpy for processing (creates copy)\n            pixels = np.frombuffer(pixels_view, dtype=np.uint8).reshape((height, width, 3))\n            \n            print(f\"Received frame {frame['__metadata__']['frame_id']}: {width}x{height}\")\n            \n            # Process frame...\n        \n        time.sleep(1/30)\n```\n\n### Example 2: Sensor Data with Control Messages\n\n```python\nimport shmx\nimport struct\nimport time\n\n# Control message types\nCTRL_SET_RATE = 0x1001\nCTRL_RESET = 0x1002\n\n# Server with control message handling\ndef sensor_server():\n    streams = [\n        shmx.create_stream_spec(1, \"temperature\", shmx.DT_F32, 1, 4),\n        shmx.create_stream_spec(2, \"pressure\", shmx.DT_F32, 1, 4),\n        shmx.create_stream_spec(3, \"humidity\", shmx.DT_F32, 1, 4),\n    ]\n    \n    server = shmx.Server()\n    server.create(\"sensors\", streams=streams)\n    \n    rate = 10.0  # Hz\n    \n    for i in range(1000):\n        # Poll control messages\n        msgs = server.poll_control()\n        for msg in msgs:\n            if msg['type'] == CTRL_SET_RATE:\n                new_rate = struct.unpack('f', msg['data'])[0]\n                print(f\"Reader {msg['reader_id']} set rate to {new_rate} Hz\")\n                rate = new_rate\n            elif msg['type'] == CTRL_RESET:\n                print(f\"Reader {msg['reader_id']} requested reset\")\n        \n        # Publish sensor data\n        frame = server.begin_frame()\n        server.append_stream(frame, 1, struct.pack('f', 25.0 + i * 0.1), 1)\n        server.append_stream(frame, 2, struct.pack('f', 1013.25 + i * 0.01), 1)\n        server.append_stream(frame, 3, struct.pack('f', 45.0 + i * 0.05), 1)\n        server.publish_frame(frame, time.time())\n        \n        # Check reader health\n        readers = server.snapshot_readers()\n        print(f\"Active readers: {len([r for r in readers if r['in_use']])}\")\n        \n        time.sleep(1.0 / rate)\n    \n    server.destroy()\n\n# Client with control messages\ndef sensor_client():\n    client = shmx.Client()\n    client.open(\"sensors\")\n    \n    print(\"Streams:\", client.get_streams_info())\n    \n    # Request faster rate\n    client.send_control(CTRL_SET_RATE, struct.pack('f', 100.0))\n    \n    for _ in range(100):\n        frame = client.get_latest_frame()\n        if frame:\n            temp = struct.unpack('f', frame['temperature']['data'])[0]\n            pressure = struct.unpack('f', frame['pressure']['data'])[0]\n            humidity = struct.unpack('f', frame['humidity']['data'])[0]\n            \n            print(f\"T={temp:.1f}\u00b0C P={pressure:.2f}hPa H={humidity:.1f}%\")\n        \n        time.sleep(0.01)\n    \n    client.close()\n```\n\n### Example 3: Multiple Streams with Numpy\n\n```python\nimport shmx\nimport numpy as np\nimport time\n\ndef numpy_example():\n    # Server\n    streams = [\n        shmx.create_stream_spec(1, \"positions\", shmx.DT_F32, 3, 12),\n        shmx.create_stream_spec(2, \"velocities\", shmx.DT_F32, 3, 12),\n        shmx.create_stream_spec(3, \"ids\", shmx.DT_U32, 1, 4),\n    ]\n    \n    server = shmx.Server()\n    server.create(\"particles\", frame_bytes_cap=1024*1024, streams=streams)\n    \n    # Client\n    client = shmx.Client()\n    client.open(\"particles\")\n    \n    # Publish\n    num_particles = 1000\n    for i in range(100):\n        positions = np.random.randn(num_particles, 3).astype(np.float32)\n        velocities = np.random.randn(num_particles, 3).astype(np.float32)\n        ids = np.arange(num_particles, dtype=np.uint32)\n        \n        frame = server.begin_frame()\n        server.append_stream(frame, 1, positions.tobytes(), num_particles)\n        server.append_stream(frame, 2, velocities.tobytes(), num_particles)\n        server.append_stream(frame, 3, ids.tobytes(), num_particles)\n        server.publish_frame(frame, i * 0.01)\n        \n        # Read back\n        frame_data = client.get_latest_frame()\n        if frame_data:\n            # Zero-copy view into shared memory\n            pos_view = np.frombuffer(frame_data['positions']['data'], dtype=np.float32)\n            pos_array = pos_view.reshape(-1, 3)\n            \n            vel_view = np.frombuffer(frame_data['velocities']['data'], dtype=np.float32)\n            vel_array = vel_view.reshape(-1, 3)\n            \n            print(f\"Frame {i}: {len(pos_array)} particles\")\n            print(f\"  Position range: [{pos_array.min():.2f}, {pos_array.max():.2f}]\")\n        \n        time.sleep(0.01)\n    \n    server.destroy()\n    client.close()\n\nif __name__ == '__main__':\n    numpy_example()\n```\n\n### Example 4: Inspector for Debugging\n\n```python\nimport shmx\nimport time\n\ndef inspect_stream(name):\n    \"\"\"Inspect a running stream without interfering\"\"\"\n    inspector = shmx.Inspector()\n    \n    if not inspector.open(name):\n        print(f\"Failed to open stream '{name}'\")\n        return\n    \n    # Get complete report\n    report = inspector.inspect()\n    \n    print(f\"\\n=== Stream Inspection: {name} ===\")\n    print(f\"Session ID: {report['session_id']}\")\n    print(f\"Frame Sequence: {report['frame_seq']}\")\n    print(f\"Readers Connected: {report['readers_connected']}\")\n    print(f\"Static Generation: {report['static_gen']}\")\n    \n    print(\"\\nStreams:\")\n    for stream in report['streams']:\n        print(f\"  [{stream['id']}] {stream['name']}\")\n        print(f\"      Type: {stream['dtype']} x {stream['components']}\")\n        print(f\"      Layout: {stream['layout']}\")\n        print(f\"      Bytes/elem: {stream['bytes_per_elem']}\")\n    \n    print(\"\\nReaders:\")\n    for reader in report['readers']:\n        if reader['in_use']:\n            print(f\"  Reader {reader['reader_id']}\")\n            print(f\"    Last frame: {reader['last_frame_seen']}\")\n            print(f\"    Heartbeat: {reader['heartbeat']}\")\n    \n    inspector.close()\n\n# Usage\nif __name__ == '__main__':\n    inspect_stream(\"my_stream\")\n```\n\n### Example 5: Multi-Process Communication\n\n```python\nimport shmx\nimport multiprocessing\nimport time\nimport struct\n\ndef producer_process(name):\n    \"\"\"Producer process\"\"\"\n    streams = [\n        shmx.create_stream_spec(1, \"counter\", shmx.DT_U64, 1, 8),\n        shmx.create_stream_spec(2, \"value\", shmx.DT_F64, 1, 8),\n    ]\n    \n    server = shmx.Server()\n    server.create(name, streams=streams)\n    print(f\"Producer: Started on '{name}'\")\n    \n    for i in range(100):\n        frame = server.begin_frame()\n        server.append_stream(frame, 1, struct.pack('Q', i), 1)\n        server.append_stream(frame, 2, struct.pack('d', i * 3.14), 1)\n        server.publish_frame(frame, time.time())\n        time.sleep(0.1)\n    \n    server.destroy()\n    print(\"Producer: Done\")\n\ndef consumer_process(name, consumer_id):\n    \"\"\"Consumer process\"\"\"\n    time.sleep(0.5)  # Wait for producer\n    \n    client = shmx.Client()\n    if not client.open(name):\n        print(f\"Consumer {consumer_id}: Failed to connect\")\n        return\n    \n    print(f\"Consumer {consumer_id}: Connected to '{name}'\")\n    \n    count = 0\n    for _ in range(50):\n        frame = client.get_latest_frame()\n        if frame:\n            counter = struct.unpack('Q', frame['counter']['data'])[0]\n            value = struct.unpack('d', frame['value']['data'])[0]\n            print(f\"Consumer {consumer_id}: counter={counter}, value={value:.2f}\")\n            count += 1\n        time.sleep(0.2)\n    \n    client.close()\n    print(f\"Consumer {consumer_id}: Received {count} frames\")\n\nif __name__ == '__main__':\n    stream_name = \"multi_process_test\"\n    \n    # Start producer\n    producer = multiprocessing.Process(target=producer_process, args=(stream_name,))\n    producer.start()\n    \n    # Start multiple consumers\n    consumers = []\n    for i in range(3):\n        consumer = multiprocessing.Process(target=consumer_process, args=(stream_name, i))\n        consumer.start()\n        consumers.append(consumer)\n    \n    # Wait for completion\n    producer.join()\n    for consumer in consumers:\n        consumer.join()\n    \n    print(\"All processes completed\")\n```\n\n## \ud83d\udd27 Advanced Usage\n\n### Memory Layout and Performance\n\nSHMX uses a ring buffer architecture with the following characteristics:\n\n- **Slots**: Number of frames buffered (configure with `slots` parameter)\n- **Frame Size**: Maximum frame payload size (`frame_bytes_cap`)\n- **Zero-Copy**: Client `memoryview` objects point directly to shared memory\n- **Lock-Free**: Atomic operations for synchronization\n\n**Performance Tips:**\n1. Allocate `frame_bytes_cap` based on your maximum frame size\n2. Use `slots=3` or `slots=4` for typical applications\n3. `memoryview` data is only valid until the next server frame publication\n4. Convert to `bytes()` or `bytearray()` if you need to retain data\n\n### Error Handling\n\n```python\nimport shmx\n\n# Always check return values\nserver = shmx.Server()\nif not server.create(\"my_stream\", ...):\n    print(\"Failed to create server - check permissions and naming\")\n    exit(1)\n\nclient = shmx.Client()\nif not client.open(\"my_stream\"):\n    print(\"Failed to open client - server may not be running\")\n    exit(1)\n\n# Check for None when reading frames\nframe = client.get_latest_frame()\nif frame is None:\n    print(\"No frame available or checksum mismatch\")\n```\n\n### Schema Evolution\n\n```python\n# Server can update static metadata\nserver.write_static_append(b\"extra_metadata\", len(b\"extra_metadata\"))\n\n# Clients can refresh\nclient.refresh_static()\nstreams = client.get_streams_info()\n```\n\n## \ud83d\udc1b Troubleshooting\n\n### Common Issues\n\n1. **\"Failed to create server\"**\n   - Check if name is already in use\n   - Ensure sufficient permissions\n   - On Linux: Check `/dev/shm/` permissions\n\n2. **\"Failed to open client\"**\n   - Verify server is running\n   - Check that names match exactly\n   - Ensure client has read permissions\n\n3. **Frames are None**\n   - Server may not be publishing yet\n   - Check frame checksums (possible corruption)\n   - Verify session IDs match\n\n4. **Memory issues**\n   - Increase `frame_bytes_cap` if frames are too large\n   - Check total shared memory usage\n   - On Linux: `df -h /dev/shm`\n\n## \ud83d\udcca Performance Characteristics\n\n- **Latency**: < 1 microsecond for local reads (CPU cache hit)\n- **Throughput**: Limited by memory bandwidth (10+ GB/s typical)\n- **Scalability**: Supports 16+ concurrent readers (configurable)\n- **Overhead**: Minimal - atomic operations only\n\n## \ud83d\udd17 Related Projects\n\n- Main repository: [github.com/HinaPE/shared-mem-ipc](https://github.com/HinaPE/shared-mem-ipc)\n- C++ header-only library included\n\n## \ud83d\udcc4 License\n\nMozilla Public License Version 2.0\n\n## \ud83e\udd1d Contributing\n\nContributions are welcome! Please visit the main repository for guidelines.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "High-performance shared-memory IPC for frame streaming",
    "version": "1.0.1",
    "project_urls": null,
    "split_keywords": [
        "ipc",
        " shared-memory",
        " high-performance",
        " streaming",
        " frame"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "6ad93f08ba68112fe4338832832436338035f945e3b9b61bde6784321f3141d0",
                "md5": "4d3f18c846d8e76a1fd053fe5295c28e",
                "sha256": "cea25fd392c438259b76dc5f41fb71d16304d590204457a29dafcc7413544b3d"
            },
            "downloads": -1,
            "filename": "shmx-1.0.1-cp313-cp313-macosx_11_0_arm64.whl",
            "has_sig": false,
            "md5_digest": "4d3f18c846d8e76a1fd053fe5295c28e",
            "packagetype": "bdist_wheel",
            "python_version": "cp313",
            "requires_python": ">=3.7",
            "size": 90511,
            "upload_time": "2025-10-13T04:31:46",
            "upload_time_iso_8601": "2025-10-13T04:31:46.877864Z",
            "url": "https://files.pythonhosted.org/packages/6a/d9/3f08ba68112fe4338832832436338035f945e3b9b61bde6784321f3141d0/shmx-1.0.1-cp313-cp313-macosx_11_0_arm64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "bf2c0db5d4b820a6c6515c9f8b36ad6c76c48e92c467ba3d88f18ce85ba0488e",
                "md5": "edb8991210f865a6aa5c76fc78a2affe",
                "sha256": "86e2104090c21f1a543cb9680ccee886bf52a97fd3e8f1429f011a9d2fb513b7"
            },
            "downloads": -1,
            "filename": "shmx-1.0.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
            "has_sig": false,
            "md5_digest": "edb8991210f865a6aa5c76fc78a2affe",
            "packagetype": "bdist_wheel",
            "python_version": "cp313",
            "requires_python": ">=3.7",
            "size": 116504,
            "upload_time": "2025-10-13T04:31:48",
            "upload_time_iso_8601": "2025-10-13T04:31:48.598730Z",
            "url": "https://files.pythonhosted.org/packages/bf/2c/0db5d4b820a6c6515c9f8b36ad6c76c48e92c467ba3d88f18ce85ba0488e/shmx-1.0.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "78d72ead60ed8084ef57f3f858eaf2b1a369f5d0a4bc55ed4338923f220aa784",
                "md5": "391aea7c4d6e3ec54b179326e9bd5e30",
                "sha256": "38ffd180ce03427522781915e463040beefaa4086fed0ac7bad187999116e638"
            },
            "downloads": -1,
            "filename": "shmx-1.0.1-cp313-cp313-win_amd64.whl",
            "has_sig": false,
            "md5_digest": "391aea7c4d6e3ec54b179326e9bd5e30",
            "packagetype": "bdist_wheel",
            "python_version": "cp313",
            "requires_python": ">=3.7",
            "size": 102876,
            "upload_time": "2025-10-13T04:31:50",
            "upload_time_iso_8601": "2025-10-13T04:31:50.111818Z",
            "url": "https://files.pythonhosted.org/packages/78/d7/2ead60ed8084ef57f3f858eaf2b1a369f5d0a4bc55ed4338923f220aa784/shmx-1.0.1-cp313-cp313-win_amd64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "90072bb32a2c96a4c376de013b379647e44c208857c28d0b87b3cca95b5b7f33",
                "md5": "c6b9e006bb169f8dcf0c6a7edfae6b7e",
                "sha256": "933fa5d113cdb992618363ca40cdd180cfc741ea5bd0c166450ae445c749b50e"
            },
            "downloads": -1,
            "filename": "shmx-1.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "c6b9e006bb169f8dcf0c6a7edfae6b7e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 31532,
            "upload_time": "2025-10-13T04:31:52",
            "upload_time_iso_8601": "2025-10-13T04:31:52.151024Z",
            "url": "https://files.pythonhosted.org/packages/90/07/2bb32a2c96a4c376de013b379647e44c208857c28d0b87b3cca95b5b7f33/shmx-1.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-13 04:31:52",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "shmx"
}
        
Elapsed time: 1.66838s