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
14 changes: 9 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.9', '3.10', '3.11']
python: ['3.10', '3.11']

steps:
- uses: compas-dev/compas-actions.build@v3
- uses: compas-dev/compas-actions.build@v4
with:
python: ${{ matrix.python }}
invoke_lint: true
Expand All @@ -33,16 +33,20 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install dependencies
shell: cmd
run: |
curl -o compas.tar.gz -LJO https://pypi.debian.net/compas/latest
curl -o ironpython-pytest.tar.gz -LJO https://pypi.debian.net/ironpython-pytest/latest
choco install ironpython --version=2.7.8.1
ipy -X:Frames -m ensurepip
ipy -X:Frames -m pip install --no-deps compas.tar.gz
ipy -X:Frames -m pip install --no-deps ironpython-pytest.tar.gz

rem untar and rename, these cannot be installed using ironpip because they not longer have a setup.py
tar -xf compas.tar.gz && for /d %%i in (compas-*) do ren "%%i" compas

- name: Run tests
env:
IRONPYTHONPATH: ./src
IRONPYTHONPATH: ./src;./compas/src
run: |
ipy -m pytest tests/unit

Expand All @@ -54,7 +58,7 @@ jobs:
run: |
docker run -d --name nanomq -p 1883:1883 -p 8083:8083 -p 8883:8883 emqx/nanomq:latest
docker ps -a
- uses: compas-dev/compas-actions.build@v3
- uses: compas-dev/compas-actions.build@v4
with:
python: '3.11'
invoke_lint: false
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.9', '3.10', '3.11']
python: ['3.10', '3.11']

steps:
- uses: compas-dev/compas-actions.build@v3
- uses: compas-dev/compas-actions.build@v4
with:
python: ${{ matrix.python }}
invoke_lint: true
Expand All @@ -31,7 +31,7 @@ jobs:
run: |
docker run -d --name nanomq -p 1883:1883 -p 8083:8083 -p 8883:8883 emqx/nanomq:latest
docker ps -a
- uses: compas-dev/compas-actions.build@v3
- uses: compas-dev/compas-actions.build@v4
with:
python: '3.11'
invoke_lint: false
Expand Down Expand Up @@ -100,7 +100,6 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install wheel
python -m pip install --no-cache-dir -r requirements-dev.txt

- uses: NuGet/[email protected]
- name: Install dependencies
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Added support for MQTT-PAHO 2.0 versioned callbacks.

### Changed

* Updated dependency on `paho-mqtt` to support `>=1, <3` to include version `2.x` with backward compatibility.

### Removed


Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ In short, this is how that works.
3. Install development dependencies:

```bash
pip install -r requirements-dev.txt
pip install -e .[dev]
```

4. Make sure all tests pass:
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ include AUTHORS.md
include CHANGELOG.md
include requirements.txt

recursive-include examples *.py

exclude compas_eve.jpg
exclude requirements-dev.txt
exclude pytest.ini .bumpversion.cfg .editorconfig
Expand Down
64 changes: 64 additions & 0 deletions examples/mqtt_compatibility_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""
Demonstration script to show MQTT-PAHO 2.0 compatibility.

This script demonstrates that the MqttTransport can work with both
paho-mqtt 1.x and 2.x versions automatically.
"""

import sys
import os

# Add the src directory to the path to import compas_eve
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))

def main():
print("COMPAS Eve MQTT-PAHO 2.0 Compatibility Demonstration")
print("=" * 55)

try:
# Import and check version compatibility
from compas_eve.mqtt.mqtt_paho import PAHO_MQTT_V2_AVAILABLE
import paho.mqtt

print(f"paho-mqtt version: {paho.mqtt.__version__}")
print(f"MQTT-PAHO 2.0 support available: {PAHO_MQTT_V2_AVAILABLE}")
print()

# Try to create transport (will fail due to network but shows client creation works)
try:
from compas_eve.mqtt.mqtt_paho import MqttTransport
print("Attempting to create MqttTransport (will fail due to no broker)...")
transport = MqttTransport('nonexistent-broker.local')
print("✓ Transport created successfully")
except Exception as e:
if "No address associated with hostname" in str(e) or "gaierror" in str(e):
print("✓ Client creation successful (expected network error)")
else:
print(f"❌ Unexpected error: {e}")
raise

print()
print("Compatibility verification:")
if PAHO_MQTT_V2_AVAILABLE:
from paho.mqtt.enums import CallbackAPIVersion
print(f"✓ Using MQTT-PAHO 2.0 with CallbackAPIVersion.VERSION1")
print(f"✓ CallbackAPIVersion available: {hasattr(CallbackAPIVersion, 'VERSION1')}")
else:
print("✓ Using MQTT-PAHO 1.x legacy mode")
print("✓ No CallbackAPIVersion required")

print()
print("🎉 All compatibility checks passed!")

except ImportError as e:
print(f"❌ Import error: {e}")
return 1
except Exception as e:
print(f"❌ Unexpected error: {e}")
return 1

return 0

if __name__ == "__main__":
sys.exit(main())
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
compas>=1.17.6
paho-mqtt >=1, <2
paho-mqtt >=1, <3
12 changes: 11 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@ def read(*names, **kwargs):

long_description = read("README.md")
requirements = read("requirements.txt").split("\n")
optional_requirements = {}

# Read dev requirements for optional dependencies
dev_requirements = [
line.strip()
for line in read("requirements-dev.txt").split("\n")
if line.strip() and not line.strip().startswith("#") and line.strip() != "-e ."
]

optional_requirements = {
"dev": dev_requirements,
}

setup(
name="compas_eve",
Expand Down
20 changes: 18 additions & 2 deletions src/compas_eve/mqtt/mqtt_paho.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
from ..event_emitter import EventEmitterMixin

import paho.mqtt.client as mqtt
import uuid

try:
from paho.mqtt.enums import CallbackAPIVersion

PAHO_MQTT_V2_AVAILABLE = True
except ImportError:
PAHO_MQTT_V2_AVAILABLE = False


class MqttTransport(Transport, EventEmitterMixin):
Expand All @@ -14,15 +22,23 @@ class MqttTransport(Transport, EventEmitterMixin):
you are running a local broker on your machine.
port : int
MQTT broker port, defaults to ``1883``.
client_id : str, optional
Client ID for the MQTT connection. If not provided, a unique ID will be generated.
"""

def __init__(self, host, port=1883, *args, **kwargs):
def __init__(self, host, port=1883, client_id=None, *args, **kwargs):
super(MqttTransport, self).__init__(*args, **kwargs)
self.host = host
self.port = port
self._is_connected = False
self._local_callbacks = {}
self.client = mqtt.Client() # todo: generate client_id
# Generate client ID if not provided
if client_id is None:
client_id = "compas_eve_{}".format(uuid.uuid4().hex[:8])
if PAHO_MQTT_V2_AVAILABLE:
self.client = mqtt.Client(client_id=client_id, callback_api_version=CallbackAPIVersion.VERSION1)
else:
self.client = mqtt.Client(client_id=client_id)
self.client.on_connect = self._on_connect
self.client.connect(self.host, self.port)
self.client.loop_start()
Expand Down
11 changes: 10 additions & 1 deletion tests/integration/test_mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@
from compas_eve import set_default_transport
from compas_eve.mqtt import MqttTransport

HOST = "broker.hivemq.com"
HOST = "localhost"


def test_client_id():
custom_client_id = "my_custom_client_id"
transport = MqttTransport(HOST, client_id=custom_client_id)
assert transport.client._client_id == custom_client_id.encode("utf-8")

transport = MqttTransport(HOST, client_id=None)
assert transport.client._client_id.startswith("compas_eve_".encode("utf-8"))


def test_default_transport_publishing():
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/test_mqtt_paho_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import sys

if sys.platform != "cli":
import pytest
from unittest.mock import Mock, patch
from compas_eve.mqtt.mqtt_paho import MqttTransport, PAHO_MQTT_V2_AVAILABLE

def test_paho_mqtt_v1_compatibility():
with patch("compas_eve.mqtt.mqtt_paho.PAHO_MQTT_V2_AVAILABLE", False), patch(
"paho.mqtt.client.Client"
) as mock_client_class:

mock_client = Mock()
mock_client_class.return_value = mock_client

# This should work as if paho-mqtt 1.x is installed
transport = MqttTransport("localhost")

# Should have called mqtt.Client() with client_id parameter only (no callback_api_version)
mock_client_class.assert_called_once()
call_args = mock_client_class.call_args
assert "client_id" in call_args.kwargs
assert call_args.kwargs["client_id"].startswith("compas_eve_")
assert "callback_api_version" not in call_args.kwargs
assert transport.client == mock_client

def test_paho_mqtt_v2_compatibility():
if not PAHO_MQTT_V2_AVAILABLE:
pytest.skip("paho-mqtt 2.x not available in this environment")

with patch("paho.mqtt.client.Client") as mock_client_class:
from paho.mqtt.enums import CallbackAPIVersion

mock_client = Mock()
mock_client_class.return_value = mock_client

# This should work as if paho-mqtt 2.x is installed
transport = MqttTransport("localhost")

# Should have called mqtt.Client() with both client_id and callback_api_version parameters
mock_client_class.assert_called_once()
call_args = mock_client_class.call_args
assert "client_id" in call_args.kwargs
assert call_args.kwargs["client_id"].startswith("compas_eve_")
assert "callback_api_version" in call_args.kwargs
assert call_args.kwargs["callback_api_version"] == CallbackAPIVersion.VERSION1
assert transport.client == mock_client
Loading