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
4 changes: 3 additions & 1 deletion examples/cameraServiceExample.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from uuid import uuid4

from context_logger import get_logger, setup_logging

from examples import setup_shutdown
Expand All @@ -15,7 +17,7 @@ def main() -> None:
group = Group(name='effectiverange/sniper', url='udp://239.0.1.1:5555')

# Define the service information for the camera
info = ServiceInfo(name='er-sniper-camera-1', role='camera', urls={
info = ServiceInfo(uuid=uuid4(), name='er-sniper-camera-1', role='camera', urls={
'device-api': 'grpc://er-sniper-camera-1/device',
'video-stream': 'blob:http://er-sniper-camera-1/video'
})
Expand Down
4 changes: 4 additions & 0 deletions hello/advertizer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2024 Ferenc Nandor Janky <ferenj@effective-range.com>
# SPDX-FileCopyrightText: 2024 Attila Gombos <attila.gombos@effective-range.com>
# SPDX-License-Identifier: MIT

import random
import time
from typing import Any
Expand Down
4 changes: 4 additions & 0 deletions hello/api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2024 Ferenc Nandor Janky <ferenj@effective-range.com>
# SPDX-FileCopyrightText: 2024 Attila Gombos <attila.gombos@effective-range.com>
# SPDX-License-Identifier: MIT

from dataclasses import dataclass
from typing import Any

Expand Down
19 changes: 12 additions & 7 deletions hello/discoverer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# SPDX-FileCopyrightText: 2024 Ferenc Nandor Janky <ferenj@effective-range.com>
# SPDX-FileCopyrightText: 2024 Attila Gombos <attila.gombos@effective-range.com>
# SPDX-License-Identifier: MIT

from dataclasses import dataclass
from enum import Enum
from typing import Any, Protocol
from uuid import UUID

from common_utility import IReusableTimer
from context_logger import get_logger
Expand Down Expand Up @@ -36,7 +41,7 @@ def stop(self) -> None:
def discover(self, query: ServiceQuery | None = None) -> None:
raise NotImplementedError()

def get_services(self) -> dict[str, ServiceInfo]:
def get_services(self) -> dict[UUID, ServiceInfo]:
raise NotImplementedError()

def register(self, handler: OnDiscoveryEvent) -> None:
Expand All @@ -56,7 +61,7 @@ def __init__(self, sender: Sender, receiver: Receiver) -> None:
self._receiver = receiver
self._group: Group | None = None
self._matcher: ServiceMatcher | None = None
self._cache: dict[str, ServiceInfo] = {}
self._cache: dict[UUID, ServiceInfo] = {}
self._handlers: list[OnDiscoveryEvent] = []

def __enter__(self) -> Discoverer:
Expand Down Expand Up @@ -88,7 +93,7 @@ def discover(self, query: ServiceQuery | None = None) -> None:
else:
log.warning('Cannot discover services, discoverer not started', query=query)

def get_services(self) -> dict[str, ServiceInfo]:
def get_services(self) -> dict[UUID, ServiceInfo]:
return self._cache.copy()

def register(self, handler: OnDiscoveryEvent) -> None:
Expand All @@ -102,14 +107,14 @@ def get_handlers(self) -> list[OnDiscoveryEvent]:

def _handle_message(self, message: dict[str, Any]) -> None:
try:
service = ServiceInfo(**message)
service = ServiceInfo(UUID(message['uuid']), message['name'], message['role'], message.get('urls', {}))
self._handle_service(service)
except Exception as error:
log.warn('Failed to handle received message', data=message, error=error)

def _handle_service(self, service: ServiceInfo) -> None:
if self._matcher and self._matcher.matches(service):
cached = self._cache.get(service.name)
cached = self._cache.get(service.uuid)

if event := self._create_event(cached, service):
self._handle_event(event)
Expand All @@ -128,7 +133,7 @@ def _create_event(self, cached: ServiceInfo | None, service: ServiceInfo) -> Dis

def _handle_event(self, event: DiscoveryEvent) -> None:
service = event.service
self._cache[service.name] = service
self._cache[service.uuid] = service
for callback in self._handlers:
try:
callback(event)
Expand Down Expand Up @@ -158,7 +163,7 @@ def stop(self) -> None:
def discover(self, query: ServiceQuery | None = None) -> None:
self._discoverer.discover(query)

def get_services(self) -> dict[str, ServiceInfo]:
def get_services(self) -> dict[UUID, ServiceInfo]:
return self._discoverer.get_services()

def register(self, handler: OnDiscoveryEvent) -> None:
Expand Down
4 changes: 4 additions & 0 deletions hello/group.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2024 Ferenc Nandor Janky <ferenj@effective-range.com>
# SPDX-FileCopyrightText: 2024 Attila Gombos <attila.gombos@effective-range.com>
# SPDX-License-Identifier: MIT

from dataclasses import dataclass
from enum import Enum

Expand Down
4 changes: 4 additions & 0 deletions hello/receiver.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2024 Ferenc Nandor Janky <ferenj@effective-range.com>
# SPDX-FileCopyrightText: 2024 Attila Gombos <attila.gombos@effective-range.com>
# SPDX-License-Identifier: MIT

from concurrent.futures import ThreadPoolExecutor
from typing import Any, Protocol

Expand Down
4 changes: 4 additions & 0 deletions hello/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2024 Ferenc Nandor Janky <ferenj@effective-range.com>
# SPDX-FileCopyrightText: 2024 Attila Gombos <attila.gombos@effective-range.com>
# SPDX-License-Identifier: MIT

from typing import TypeVar, Generic, Any

from common_utility import IReusableTimer
Expand Down
8 changes: 8 additions & 0 deletions hello/sender.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2024 Ferenc Nandor Janky <ferenj@effective-range.com>
# SPDX-FileCopyrightText: 2024 Attila Gombos <attila.gombos@effective-range.com>
# SPDX-License-Identifier: MIT

from typing import Any, cast

from context_logger import get_logger
Expand Down Expand Up @@ -65,6 +69,10 @@ def send(self, data: Any) -> None:
def _convert_to_dict(self, data: Any) -> dict[str, Any] | None:
if isinstance(data, dict):
return data
elif hasattr(data, 'to_dict') and callable(getattr(data, 'to_dict')):
return cast(dict[str, Any], data.to_dict())
elif hasattr(data, 'as_dict') and callable(getattr(data, 'as_dict')):
return cast(dict[str, Any], data.as_dict())
elif hasattr(data, '__dict__'):
return cast(dict[str, Any], data.__dict__)
return None
Expand Down
18 changes: 18 additions & 0 deletions hello/service.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
# SPDX-FileCopyrightText: 2024 Ferenc Nandor Janky <ferenj@effective-range.com>
# SPDX-FileCopyrightText: 2024 Attila Gombos <attila.gombos@effective-range.com>
# SPDX-License-Identifier: MIT

import re
from dataclasses import dataclass, field
from typing import Any
from uuid import UUID


@dataclass
class ServiceInfo:
uuid: UUID
name: str
role: str
urls: dict[str, str] = field(default_factory=dict)

def __repr__(self) -> str:
return f"ServiceInfo(uuid='{self.uuid}', name='{self.name}', role='{self.role}', urls='{self.urls}')"

def to_dict(self) -> dict[str, Any]:
return {
'uuid': str(self.uuid),
'name': self.name,
'role': self.role,
'urls': self.urls
}


@dataclass
class ServiceQuery(object):
Expand Down
31 changes: 15 additions & 16 deletions tests/advertizerIntegrationTest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest
from unittest import TestCase
from uuid import uuid4

from common_utility import ReusableTimer
from context_logger import setup_logging
Expand All @@ -10,18 +11,17 @@
ServiceQuery, ScheduledAdvertizer

GROUP = Group('test-group', 'udp://239.0.0.1:5555')
SERVICE_INFO = ServiceInfo(uuid4(), 'test-service', 'test-role', {'test': 'http://localhost:8080'})


class AdvertizerIntegrationTest(TestCase):
SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'})

@classmethod
def setUpClass(cls):
setup_logging('hello', 'DEBUG', warn_on_overwrite=False)

def setUp(self):
print()
self.SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'})

def test_sends_hello_when_advertises_service(self):
# Given
Expand All @@ -35,12 +35,12 @@ def test_sends_hello_when_advertises_service(self):
advertizer.start(GROUP)

# When
advertizer.advertise(self.SERVICE_INFO)
advertizer.advertise(SERVICE_INFO)

wait_for_assertion(0.1, lambda: self.assertEqual(1, len(messages)))

# Then
self.assertEqual([self.SERVICE_INFO.__dict__], messages)
self.assertEqual([SERVICE_INFO.to_dict()], messages)

def test_sends_hello_when_query_received(self):
# Given
Expand All @@ -56,15 +56,15 @@ def test_sends_hello_when_query_received(self):
test_receiver.start(GROUP.hello())
test_receiver.register(lambda message: messages.append(message))

advertizer.start(GROUP, self.SERVICE_INFO)
advertizer.start(GROUP, SERVICE_INFO)

# When
test_sender.send(ServiceQuery('test-service', 'test-role'))

wait_for_assertion(0.1, lambda: self.assertEqual(1, len(messages)))

# Then
self.assertEqual([self.SERVICE_INFO.__dict__], messages)
self.assertEqual([SERVICE_INFO.to_dict()], messages)

def test_sends_hello_when_info_changed_and_query_received(self):
# Given
Expand All @@ -81,15 +81,17 @@ def test_sends_hello_when_info_changed_and_query_received(self):
test_receiver.register(lambda message: messages.append(message))

advertizer.start(GROUP)
advertizer.advertise(self.SERVICE_INFO)
advertizer.advertise(SERVICE_INFO)

query = ServiceQuery('test-service', 'test-role')
test_sender.send(query)

wait_for_assertion(0.1, lambda: self.assertEqual(2, len(messages)))

self.SERVICE_INFO.urls['test'] = 'http://localhost:9090'
advertizer.advertise(self.SERVICE_INFO)
new_service_info = ServiceInfo(
SERVICE_INFO.uuid, SERVICE_INFO.name, SERVICE_INFO.role, {'test': 'http://localhost:9090'}
)
advertizer.advertise(new_service_info)

# When
test_sender.send(query)
Expand All @@ -98,10 +100,7 @@ def test_sends_hello_when_info_changed_and_query_received(self):

# Then
self.assertEqual([
{'name': 'test-service', 'role': 'test-role', 'urls': {'test': 'http://localhost:8080'}},
{'name': 'test-service', 'role': 'test-role', 'urls': {'test': 'http://localhost:8080'}},
{'name': 'test-service', 'role': 'test-role', 'urls': {'test': 'http://localhost:9090'}},
{'name': 'test-service', 'role': 'test-role', 'urls': {'test': 'http://localhost:9090'}}
SERVICE_INFO.to_dict(), SERVICE_INFO.to_dict(), new_service_info.to_dict(), new_service_info.to_dict()
], messages)

def test_sends_hello_when_schedules_advertisement_once(self):
Expand All @@ -118,12 +117,12 @@ def test_sends_hello_when_schedules_advertisement_once(self):
scheduled_advertizer.start(GROUP)

# When
scheduled_advertizer.schedule(self.SERVICE_INFO, interval=0.01, one_shot=True)
scheduled_advertizer.schedule(SERVICE_INFO, interval=0.01, one_shot=True)

wait_for_assertion(0.1, lambda: self.assertEqual(1, len(messages)))

# Then
self.assertEqual([self.SERVICE_INFO.__dict__], messages)
self.assertEqual([SERVICE_INFO.to_dict()], messages)

def test_sends_hello_when_schedules_advertisement_periodically(self):
# Given
Expand All @@ -139,7 +138,7 @@ def test_sends_hello_when_schedules_advertisement_periodically(self):
scheduled_advertizer.start(GROUP)

# When
scheduled_advertizer.schedule(self.SERVICE_INFO, interval=0.01)
scheduled_advertizer.schedule(SERVICE_INFO, interval=0.01)

# Then
wait_for_assertion(0.1, lambda: self.assertEqual(5, len(messages)))
Expand Down
Loading
Loading