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
1 change: 1 addition & 0 deletions .devcontainer
Submodule .devcontainer added at 0ddb12
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "daily"
17 changes: 17 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: CI

on:
push:
branches: [main]
tags: [v*.*.*]

pull_request:
branches: [ "main" ]
types:
- synchronize
- opened
- reopened

jobs:
call_ci:
uses: EffectiveRange/ci-workflows/.github/workflows/python-ci.yaml@latest-python
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule ".devcontainer"]
path = .devcontainer
url = https://github.com/EffectiveRange/devcontainer-defs
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions .idea/python-hello.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"args": [
"--backend=scipy"
],
"console": "integratedTerminal"
},
{
"name": "Run All Tests (pytest)",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": [
"tests"
],
"console": "integratedTerminal"
},
{
"name": "Run All Tests with Coverage (pytest-cov)",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": [
"--cov=hello", "tests"
],
"console": "integratedTerminal"
}
]
}
20 changes: 20 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"python.venvPath": "${workspaceFolder}/.venv",
"python.testing.unittestArgs": [
"-v",
"-s",
"./tests",
"-p",
"*Test.py"
],
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"black-formatter.interpreter": [
"${workspaceFolder}/.venv/bin/python3"
],
"black-formatter.args": [
"--config=setup.cfg"
],
"python.analysis.typeCheckingMode": "standard",
"python.testing.pytestArgs": []
}
15 changes: 15 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Create Python venv",
"type": "shell",
"command": "if [ -d /var/chroot/buildroot ];then dpkgdeps -v --arch $(grep TARGET_ARCH /home/crossbuilder/target/target | cut -d'=' -f2 | tr -d \\') .;else dpkgdeps -v .;fi && rm -rf .venv && python3 -m venv --system-site-packages .venv && .venv/bin/pip install -e . && .venv/bin/python3 -m mypy --non-interactive --install-types && .venv/bin/pip install pytest-cov || true",
"group": "build",
"detail": "Creates a Python virtual environment in the .venv folder",
"problemMatcher": [
"$eslint-compact"
]
}
]
}
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[![CI](https://github.com/EffectiveRange/python-hello/actions/workflows/ci.yaml/badge.svg)](https://github.com/EffectiveRange/python-hello/actions/workflows/ci.yaml)
[![Coverage badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/EffectiveRange/python-hello/python-coverage-comment-action-data/endpoint.json)](https://htmlpreview.github.io/?https://github.com/EffectiveRange/python-hello/blob/python-coverage-comment-action-data/htmlcov/index.html)

# python-hello
A service advertizer library using ZeroMQ
A service advertizer/discovery protocol library using ZeroMQ
20 changes: 20 additions & 0 deletions deps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"deps": [
{
"name": "libzmq-drafts",
"hostinstall": true
},
{
"name": "python3-zmq-drafts",
"hostinstall": true
},
{
"name": "python3-context-logger",
"hostinstall": true
},
{
"name": "python3-common-utility",
"hostinstall": true
}
]
}
7 changes: 7 additions & 0 deletions hello/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .group import *
from .sender import *
from .receiver import *
from .service import *
from .advertizer import *
from .discoverer import *
from .api import *
130 changes: 130 additions & 0 deletions hello/advertizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import random
import time
from typing import Any

from common_utility import IReusableTimer
from context_logger import get_logger

from hello import ServiceInfo, Group, Sender, GroupAccess, Receiver, ServiceMatcher, ServiceQuery

log = get_logger('Advertizer')


class Advertizer:

def start(self, address: str, group: Group, info: ServiceInfo | None = None) -> None:
raise NotImplementedError()

def stop(self) -> None:
raise NotImplementedError()

def advertise(self, info: ServiceInfo | None = None) -> None:
raise NotImplementedError()


class DefaultAdvertizer(Advertizer):

def __init__(self, sender: Sender) -> None:
self._sender = sender
self._group: Group | None = None
self._info: ServiceInfo | None = None

def __enter__(self) -> Advertizer:
return self

def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
self.stop()

def start(self, address: str, group: Group, info: ServiceInfo | None = None) -> None:
self._sender.start(GroupAccess(address, group.hello()))
self._group = group
self._info = info

def stop(self) -> None:
self._group = None
self._sender.stop()

def advertise(self, info: ServiceInfo | None = None) -> None:
if self._group:
if info:
self._info = info
if self._info:
self._sender.send(self._info)
log.info('Service advertised', service=self._info, group=self._group)
else:
log.warning('Cannot advertise service, advertizer not started', service=info)


class RespondingAdvertizer(DefaultAdvertizer):

def __init__(self, sender: Sender, receiver: Receiver, max_response_delay: float = 0.1) -> None:
super().__init__(sender)
self._receiver = receiver
self._max_delay = max_response_delay

def start(self, address: str, group: Group, info: ServiceInfo | None = None) -> None:
super().start(address, group, info)
self._receiver.start(GroupAccess(address, group.query()))
self._receiver.register(self._handle_message)

def stop(self) -> None:
super().stop()
self._receiver.stop()

def _handle_message(self, message: dict[str, Any]) -> None:
if self._info:
try:
query = ServiceQuery(**message)
log.debug('Query received', group=self._group, query=query)
self._handle_query(query, self._info)
except Exception as error:
log.warning('Invalid query message received', group=self._group, received=message, error=error)

def _handle_query(self, query: ServiceQuery, info: ServiceInfo) -> None:
matcher = ServiceMatcher(query)
if matcher and matcher.matches(info):
delay = round(self._max_delay * random.random(), 3)
log.info('Responding to query', group=self._group, query=matcher.query, service=info, delay=delay)
time.sleep(delay)
self.advertise(info)


class ScheduledAdvertizer(Advertizer):

def schedule(self, info: ServiceInfo | None = None, interval: float = 10, one_shot: bool = False) -> None:
raise NotImplementedError()


class DefaultScheduledAdvertizer(ScheduledAdvertizer):

def __init__(self, advertizer: Advertizer, timer: IReusableTimer) -> None:
self._advertizer = advertizer
self._timer = timer

def __enter__(self) -> ScheduledAdvertizer:
return self

def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
self.stop()

def start(self, address: str, group: Group, info: ServiceInfo | None = None) -> None:
self._advertizer.start(address, group, info)

def stop(self) -> None:
self._timer.cancel()
self._advertizer.stop()

def advertise(self, info: ServiceInfo | None = None) -> None:
self._advertizer.advertise(info)

def schedule(self, info: ServiceInfo | None = None, interval: float = 60, one_shot: bool = False) -> None:
if one_shot:
self._timer.start(interval, self.advertise, [info])
log.info('One-shot service advertisement scheduled', service=info, interval=interval)
else:
self._timer.start(interval, self._advertise_and_restart, [info])
log.info('Periodic service advertisement scheduled', service=info, interval=interval)

def _advertise_and_restart(self, info: ServiceInfo | None = None) -> None:
self.advertise(info)
self._timer.restart()
44 changes: 44 additions & 0 deletions hello/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Any

from common_utility import ReusableTimer
from zmq import Context

from hello import Advertizer, Discoverer, RadioSender, DishReceiver, DefaultAdvertizer, DefaultDiscoverer, \
ScheduledAdvertizer, RespondingAdvertizer, DefaultScheduledAdvertizer


class Hello:

def default_advertizer(self, respond: bool = True, delay: float = 0.1) -> Advertizer:
raise NotImplementedError()

def scheduled_advertizer(self, respond: bool = True, delay: float = 0.1) -> ScheduledAdvertizer:
raise NotImplementedError()

def discoverer(self) -> Discoverer:
raise NotImplementedError()


class DefaultHello(Hello):

def __init__(self, context: Context[Any] | None = None, max_workers: int = 1, poll_timeout: float = 0.1) -> None:
self._context = context if context else Context()
self._max_workers = max_workers
self._poll_timeout = poll_timeout

def default_advertizer(self, respond: bool = True, delay: float = 0.1) -> Advertizer:
sender = RadioSender(self._context)
if respond:
receiver = DishReceiver(self._context, self._max_workers, self._poll_timeout)
return RespondingAdvertizer(sender, receiver, delay)
else:
return DefaultAdvertizer(sender)

def scheduled_advertizer(self, respond: bool = True, delay: float = 0.1) -> ScheduledAdvertizer:
advertizer = self.default_advertizer(respond, delay)
return DefaultScheduledAdvertizer(advertizer, ReusableTimer())

def discoverer(self) -> Discoverer:
sender = RadioSender(self._context)
receiver = DishReceiver(self._context, self._max_workers, self._poll_timeout)
return DefaultDiscoverer(sender, receiver)
Loading
Loading