# SHMX - High-Performance Shared Memory IPC
[](https://badge.fury.io/py/shmx)
[](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[](https://badge.fury.io/py/shmx)\n[](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"
}