Skip to content
Closed
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
12 changes: 12 additions & 0 deletions .github/workflows/arm-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ jobs:
# And it also fails with aarch64 binaries of postgresql.
sudo -u pi taskset -c 0 /bin/bash /opt/pynab/install.sh ci-chroot-test

- name: Retrieve coverage file
run: |
cp ${{ steps.arm_runner_tests.outputs.image_mount_path }}/opt/pynab/coverage.xml ./coverage-${{ matrix.target }}.xml || true

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage-*.xml
fail_ci_if_error: true
flags: unittests-${{ matrix.target }}
token: ${{ secrets.CODECOV_TOKEN }}

create_image:
name: Build image
runs-on: ubuntu-latest
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/python-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,26 @@ jobs:
- name: Checkout pynab
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.9'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install mypy

- name: Run Python lint checks
uses: ricardochaves/python-lint@v1.4.0
with:
python-root-list: "nab*"
use-mypy: true
use-pylint: false
use-mypy: false
# https://github.com/PyCQA/pycodestyle/issues/373

extra-pycodestyle-options: "--ignore=E121,E123,E126,E226,E24,E704,W503,W504,E203 --exclude=*/migrations/"
extra-flake8-options: "--extend-ignore=E203,E501"
extra-black-options: "-l 79"
extra-isort-options: "-l 79 --profile black --indent 4"
extra-mypy-options: "--ignore-missing-imports --exclude /migrations/"
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# Nabaztag en Python pour Raspberry Pi

[![build (qemu)](https://github.com/mtouzot/pynab/actions/workflows/arm-runner.yml/badge.svg?branch=main)](https://github.com/mtouzot/pynab/actions/workflows/arm-runner.yml)
[![tests](https://github.com/mtouzot/pynab/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/mtouzot/pynab/actions/workflows/tests.yml)
[![build (raspberry pi)](https://github.com/mtouzot/pynab/actions/workflows/arm-runner.yml/badge.svg?branch=main)](https://github.com/mtouzot/pynab/actions/workflows/arm-runner.yml)
![coverage](https://codecov.io/gh/mtouzot/pynab/branch/main/graph/badge.svg)

![lint](https://github.com/mtouzot/pynab/actions/workflows/python-lint.yml/badge.svg)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

![release](https://img.shields.io/github/v/release/mtouzot/pynab)
![python](https://img.shields.io/badge/python-3.9-blue)

![license](https://img.shields.io/github/license/mtouzot/pynab)

## Cartes

Ce système est conçu pour deux cartes pour **Nabaztag** (v1) et **Nabaztag:Tag** (v2) :
Expand Down
6 changes: 4 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -380,10 +380,12 @@ else
fi

if [ $test -eq 1 ]; then
echo "Running tests"
if [ $ci_chroot -eq 1 ]; then
sudo CI=1 venv/bin/pytest
echo "Running tests with coverage (CI chroot)"
sudo CI=1 venv/bin/coverage run -m pytest
sudo venv/bin/coverage xml -o /opt/pynab/coverage.xml
else
echo "Running tests"
sudo venv/bin/pytest
fi
fi
Expand Down
11 changes: 11 additions & 0 deletions nabcommon/nabpacketrouter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .typing import NabdPacket
from .nabregistry import NabRegistry


class NabPacketRouter:
def __init__(self, registry: NabRegistry):
self.registry = registry

async def dispatch(self, packet: NabdPacket):
if handler := self.registry.get(packet["type"]):
await handler(packet)
28 changes: 28 additions & 0 deletions nabcommon/nabregistry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import Callable, Awaitable, Dict

from .typing import NabdPacket

NabHandler = Callable[[NabdPacket], Awaitable[None]]


class NabRegistry:
"""
Registry for packet handlers
"""

def __init__(self):
self.__handlers = Dict[str, NabHandler] = {}

def register(self, packet_type: str):
"""
Register a handler for a given packet type
"""

def decorator(fn: NabHandler):
self.__handlers[packet_type] = fn
return fn

return decorator

def get(self, packet_type: str):
return self.__handlers.get(packet_type)
71 changes: 64 additions & 7 deletions nabcommon/nabservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@
import traceback
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, List, Optional, Tuple, cast
from typing import Any, List, Optional, Tuple

from lockfile import AlreadyLocked, LockFailed # type: ignore
from lockfile.pidlockfile import PIDLockFile # type: ignore

from nabcommon import nablogging, settings
from nabcommon import nablogging, nabpacketrouter, nabregistry, settings

from .typing import NabdPacket
from .typing import (
NabdPacket,
ASREventPacket,
ButtonEvenPacket,
EarEventPacket,
EarsEventPacket,
RfidEventPacket,
)


class NabService(ABC):
Expand All @@ -31,8 +38,55 @@ def __init__(self):
self.writer = None
self.loop = None
self.running = True
self.registry = nabregistry.NabRegistry()
self.router = nabpacketrouter.NabPacketRouter(self.registry)
signal.signal(signal.SIGUSR1, self.signal_handler)

def register_handlers(self):
"""
Register handlers for NabdPackets object, if any.
"""

@self.registry.register("asr_event")
async def handle_asr(packet: ASREventPacket):
await self._handle_asr_forecast(packet)

@self.registry.register("button_event")
async def handle_button(packet: ButtonEvenPacket):
await self._handle_button_event(packet)

@self.registry.register("ear_event")
async def handle_ear(packet: EarEventPacket):
await self._handle_ear_event(packet)

@self.registry.register("ears_event")
async def handle_ears(packet: EarsEventPacket):
await self._handle_ears_event(packet)

@self.registry.register("rfid_event")
async def handle_rfid(packet: RfidEventPacket):
await self._handle_rfid_forecast(packet)

@abstractmethod
async def _handle_asr_forecast(self, packet: ASREventPacket):
pass

@abstractmethod
async def _handle_button_event(self, packet: ButtonEvenPacket):
pass

@abstractmethod
async def _handle_ear_event(self, packet: EarEventPacket):
pass

@abstractmethod
async def _handle_ears_event(self, packet: EarsEventPacket):
pass

@abstractmethod
async def _handle_rfid_forecast(self, packet: RfidEventPacket):
pass

def signal_handler(self, sig, frame):
self.loop.call_soon_threadsafe(
lambda: self.loop.create_task(self.reload_config())
Expand All @@ -43,9 +97,14 @@ async def reload_config(self):
"""
Reload configuration (on USR1 signal).
"""
raise NotImplementedError("reload_config should be implemented")

async def process_nabd_packet(self, packet: NabdPacket) -> None:
pass
"""
Process a packet from nabd.
Default implementation is to dispatch it to the router.
"""
await self.router.dispatch(packet)

async def client_loop(self):
try:
Expand Down Expand Up @@ -81,9 +140,7 @@ async def client_loop(self):
try:
packet = json.loads(line.decode("utf8"))
logging.debug(f"process nabd packet: {packet}")
await self.process_nabd_packet(
cast(NabdPacket, packet)
)
await self.router.dispatch(packet)
except json.decoder.JSONDecodeError as e:
logging.error(
f"Invalid JSON packet from nabd: {line}\n{e}"
Expand Down
Loading
Loading