forked from platypush/platypush
Added commands
module.
This commit is contained in:
parent
97adc3f775
commit
efef9d7bc0
4 changed files with 172 additions and 0 deletions
0
platypush/commands/__init__.py
Normal file
0
platypush/commands/__init__.py
Normal file
78
platypush/commands/_base.py
Normal file
78
platypush/commands/_base.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
import json
|
||||||
|
from logging import getLogger, Logger
|
||||||
|
|
||||||
|
|
||||||
|
class Command(ABC):
|
||||||
|
"""
|
||||||
|
Base class for application commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
END_OF_COMMAND = b'\x00'
|
||||||
|
"""End-of-command marker."""
|
||||||
|
|
||||||
|
def __init__(self, **args) -> None:
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger(self) -> Logger:
|
||||||
|
"""
|
||||||
|
The command class logger.
|
||||||
|
"""
|
||||||
|
return getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __call__(self, app, *_, **__):
|
||||||
|
"""
|
||||||
|
Execute the command.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""
|
||||||
|
:return: A JSON representation of the command.
|
||||||
|
"""
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
'type': 'command',
|
||||||
|
'command': self.__class__.__name__,
|
||||||
|
'args': self.args,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_bytes(self):
|
||||||
|
"""
|
||||||
|
:return: A JSON representation of the command.
|
||||||
|
"""
|
||||||
|
return str(self).encode('utf-8') + self.END_OF_COMMAND
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, data: bytes) -> "Command":
|
||||||
|
"""
|
||||||
|
:param data: A JSON representation of the command.
|
||||||
|
:raise ValueError: If the data is invalid.
|
||||||
|
:return: The command instance or None if the data is invalid.
|
||||||
|
"""
|
||||||
|
import platypush.commands
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_data = json.loads(data.decode('utf-8'))
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError from e
|
||||||
|
|
||||||
|
kind = json_data.pop('type', None)
|
||||||
|
if kind != 'command':
|
||||||
|
raise ValueError(f'Invalid command type: {kind}')
|
||||||
|
|
||||||
|
command_name = json_data.get('command')
|
||||||
|
if not command_name:
|
||||||
|
raise ValueError(f'Invalid command name: {command_name}')
|
||||||
|
|
||||||
|
cmd_class = getattr(platypush.commands, command_name, None)
|
||||||
|
if not (cmd_class and issubclass(cmd_class, Command)):
|
||||||
|
raise ValueError(f'Invalid command class: {command_name}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
return cmd_class(**json_data.get('args', {}))
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(e) from e
|
69
platypush/commands/_reader.py
Normal file
69
platypush/commands/_reader.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
from logging import getLogger
|
||||||
|
from socket import socket
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from platypush.commands import Command
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
class CommandReader:
|
||||||
|
"""
|
||||||
|
Reads command objects from file-like I/O objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_max_bufsize = 8192
|
||||||
|
"""Maximum size of a command that can be queued in the stream."""
|
||||||
|
|
||||||
|
_bufsize = 1024
|
||||||
|
"""Size of the buffer used to read commands from the socket."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = getLogger(__name__)
|
||||||
|
self._buf = bytes()
|
||||||
|
|
||||||
|
def _parse_command(self, data: bytes) -> Optional[Command]:
|
||||||
|
"""
|
||||||
|
Parses a command from the received data.
|
||||||
|
|
||||||
|
:param data: Data received from the socket
|
||||||
|
:return: The parsed command
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return Command.parse(data)
|
||||||
|
except ValueError as e:
|
||||||
|
self.logger.warning('Error while parsing command: %s', e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read(self, sock: socket) -> Optional[Command]:
|
||||||
|
"""
|
||||||
|
Parses the next command from the file-like I/O object.
|
||||||
|
|
||||||
|
:param fp: The file-like I/O object to read from.
|
||||||
|
:return: The parsed command.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = sock.recv(self._bufsize)
|
||||||
|
except OSError as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'Error while reading from socket %s: %s', sock.getsockname(), e
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
for ch in data:
|
||||||
|
if bytes([ch]) == Command.END_OF_COMMAND:
|
||||||
|
cmd = self._parse_command(self._buf)
|
||||||
|
self._buf = bytes()
|
||||||
|
|
||||||
|
if cmd:
|
||||||
|
return cmd
|
||||||
|
elif len(self._buf) >= self._max_bufsize:
|
||||||
|
self.logger.warning(
|
||||||
|
'The received command is too long: length=%d', len(self._buf)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._buf = bytes()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self._buf += bytes([ch])
|
||||||
|
|
||||||
|
return None
|
25
platypush/commands/_writer.py
Normal file
25
platypush/commands/_writer.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from logging import getLogger
|
||||||
|
from socket import socket
|
||||||
|
|
||||||
|
from platypush.commands import Command
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
class CommandWriter:
|
||||||
|
"""
|
||||||
|
Writes command objects to file-like I/O objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = getLogger(__name__)
|
||||||
|
|
||||||
|
def write(self, cmd: Command, sock: socket):
|
||||||
|
"""
|
||||||
|
Writes a command to a file-like I/O object.
|
||||||
|
|
||||||
|
:param cmd: The command to write.
|
||||||
|
:param fp: The file-like I/O object to write to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
buf = cmd.to_bytes()
|
||||||
|
sock.sendall(buf)
|
Loading…
Reference in a new issue