Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
DATANET_API_KEY=ak_your_api_key_here

# JSON examples use this channel.
DATANET_CHANNEL=project.your_project_id.demo

# Binary examples use this first; if unset, they fall back to DATANET_CHANNEL.
DATANET_BINARY_CHANNEL=project.your_project_id.lighting.dmx

DATANET_DEVICE_ID=python-example
DATANET_CLIENT_ID=datanet-python-example

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ activemq-data/
!.env.example
.envrc
.venv
.venv-*/
env/
venv/
ENV/
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Changelog

## 0.1.0 - 2026-06-12

Initial public release.

- Added async DataNet WebSocket client.
- Added sync/background-thread helpers.
- Added JSON publish and subscribe support.
- Added binary publish and subscribe support.
- Added DMX and Art-Net helper utilities.
- Added runnable examples for JSON, p5-style coordinates, and binary DMX.
- Added pytest coverage for client behavior, errors, and binary helpers.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ try:
while True:
time.sleep(1)
except KeyboardInterrupt:
dn.disconnect()
dn.disconnect_sync()
```

## API reference
Expand Down Expand Up @@ -155,6 +155,28 @@ DATANET_WS_URL='ws://localhost:8080' \
python examples/publish.py
```

### Binary examples

The JSON examples use `DATANET_CHANNEL`.

The binary DMX examples use `DATANET_BINARY_CHANNEL` first, then fall back to
`DATANET_CHANNEL` if no binary channel is set. Use the same binary channel for
both publisher and subscriber:

```bash
DATANET_API_KEY='ak_local_key_here' \
DATANET_BINARY_CHANNEL='project.abc.lighting.dmx' \
python examples/binary_dmx_subscribe.py
```

In another terminal:

```bash
DATANET_API_KEY='ak_local_key_here' \
DATANET_BINARY_CHANNEL='project.abc.lighting.dmx' \
python examples/binary_dmx_publish.py
```

To drive the browser p5 visualizer demo directly with pixel coordinates:

```bash
Expand All @@ -176,6 +198,7 @@ python examples/publish_p5.py
| `await connect()` | Fetch JWT and open WebSocket |
| `connect_sync(timeout=10)` | Same, but runs in a background thread |
| `await disconnect()` | Close connection and stop run loop |
| `disconnect_sync(timeout=10)` | Close a sync/background-thread connection |
| `subscribe(channel, handler)` | Register an async message handler |
| `unsubscribe(channel, handler=None)` | Remove handler (or all) from channel |
| `await publish(channel, data, content_type=None, metadata=None)` | Send JSON, or auto-detect bytes-like binary data |
Expand Down
20 changes: 20 additions & 0 deletions datanet/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,26 @@ async def _run() -> None:
)
self._connected_event.wait(timeout=0.05)

def disconnect_sync(self, timeout: float = 10.0) -> None:
"""Disconnect from sync/background-thread code and wait for cleanup.

This is the sync counterpart to :meth:`disconnect` for programs that
used :meth:`connect_sync`. Async applications should prefer
``await disconnect()`` or the async context manager.
"""
if self._loop and self._loop.is_running():
future = asyncio.run_coroutine_threadsafe(self.disconnect(), self._loop)
future.result(timeout=timeout)
else:
asyncio.run(self.disconnect())

if (
self._thread
and self._thread.is_alive()
and threading.current_thread() is not self._thread
):
self._thread.join(timeout=timeout)

# ── Async context manager ─────────────────────────────────────────────────

async def __aenter__(self) -> "DataNet":
Expand Down
2 changes: 1 addition & 1 deletion examples/basic_subscribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ async def on_message(data: object, meta: MessageMeta) -> None:
time.sleep(0.5)
except KeyboardInterrupt:
print("\nShutting down…")
dn.disconnect()
dn.disconnect_sync()
print("Goodbye.")


Expand Down
2 changes: 1 addition & 1 deletion examples/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ async def handle_error(exc: Exception) -> None:
except KeyboardInterrupt:
print("\nShutting down…")
finally:
dn.disconnect()
dn.disconnect_sync()
print(f"Published {published} readings. Goodbye.")


Expand Down
21 changes: 18 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,30 @@ dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"aioresponses>=0.7",
"ruff>=0.8",
]

[project.urls]
Homepage = "https://datanet.art"
Repository = "https://github.com/datanet-art/datanet-python"
Documentation = "https://datanet.art/docs"
Documentation = "https://datanet.art/docs/python"
Source = "https://github.com/datanet-art/datanet-python"

[tool.hatch.build.targets.wheel]
packages = ["datanet"]

[tool.hatch.build.targets.sdist]
include = [
"/datanet",
"/examples",
"/tests",
"/LICENSE",
"/CHANGELOG.md",
"/.env.example",
"/README.md",
"/PROTOCOL.md",
"/pyproject.toml",
]

[tool.pytest.ini_options]
asyncio_mode = "auto"

Expand All @@ -51,4 +65,5 @@ target-version = "py311"
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]
select = ["E", "F"]
ignore = ["E501"]
7 changes: 7 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ async def on_message(data, meta):

self.assertEqual(seen, [])

def test_disconnect_sync_without_active_connection_is_safe(self):
client = DataNet("ak_test")

client.disconnect_sync()

self.assertFalse(client.connected)

async def test_binary_messages_dispatch_to_binary_and_any_handlers(self):
client = DataNet("ak_test")
binary_seen = []
Expand Down
Loading