generated from daniil-berg/boilerplate-py
sphinx documentation; adjusted all docstrings; moved some modules to non-public subpackage
This commit is contained in:
@ -0,0 +1,2 @@
|
||||
from .server import TCPControlServer, UnixControlServer
|
||||
from .client import TCPControlClient, UnixControlClient
|
||||
|
@ -15,7 +15,7 @@ You should have received a copy of the GNU Lesser General Public License along w
|
||||
If not, see <https://www.gnu.org/licenses/>."""
|
||||
|
||||
__doc__ = """
|
||||
CLI client entry point.
|
||||
CLI entry point script for a :class:`ControlClient`.
|
||||
"""
|
||||
|
||||
|
||||
@ -24,12 +24,15 @@ from asyncio import run
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Sequence
|
||||
|
||||
from ..constants import PACKAGE_NAME
|
||||
from ..internals.constants import PACKAGE_NAME
|
||||
from ..pool import TaskPool
|
||||
from .client import ControlClient, TCPControlClient, UnixControlClient
|
||||
from .client import TCPControlClient, UnixControlClient
|
||||
from .server import TCPControlServer, UnixControlServer
|
||||
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
CLIENT_CLASS = 'client_class'
|
||||
UNIX, TCP = 'unix', 'tcp'
|
||||
SOCKET_PATH = 'path'
|
||||
@ -39,7 +42,7 @@ HOST, PORT = 'host', 'port'
|
||||
def parse_cli(args: Sequence[str] = None) -> Dict[str, Any]:
|
||||
parser = ArgumentParser(
|
||||
prog=f'{PACKAGE_NAME}.control',
|
||||
description=f"Simple CLI based {ControlClient.__name__} for {PACKAGE_NAME}"
|
||||
description=f"Simple CLI based control client for {PACKAGE_NAME}"
|
||||
)
|
||||
subparsers = parser.add_subparsers(title="Connection types")
|
||||
|
||||
|
@ -27,13 +27,24 @@ from asyncio.streams import StreamReader, StreamWriter, open_connection
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
from ..constants import CLIENT_EXIT, CLIENT_INFO, SESSION_MSG_BYTES
|
||||
from ..types import ClientConnT, PathT
|
||||
from ..internals.constants import CLIENT_INFO, SESSION_MSG_BYTES
|
||||
from ..internals.types import ClientConnT, PathT
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ControlClient',
|
||||
'TCPControlClient',
|
||||
'UnixControlClient',
|
||||
'CLIENT_EXIT'
|
||||
]
|
||||
|
||||
|
||||
CLIENT_EXIT = 'exit'
|
||||
|
||||
|
||||
class ControlClient(ABC):
|
||||
"""
|
||||
Abstract base class for a simple implementation of a task pool control client.
|
||||
Abstract base class for a simple implementation of a pool control client.
|
||||
|
||||
Since the server's control interface is simply expecting commands to be sent, any process able to connect to the
|
||||
TCP or UNIX socket and issue the relevant commands (and optionally read the responses) will work just as well.
|
||||
@ -58,7 +69,7 @@ class ControlClient(ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, **conn_kwargs) -> None:
|
||||
"""Simply stores the connection keyword-arguments necessary for opening the connection."""
|
||||
"""Simply stores the keyword-arguments for opening the connection."""
|
||||
self._conn_kwargs = conn_kwargs
|
||||
self._connected: bool = False
|
||||
|
||||
@ -91,7 +102,7 @@ class ControlClient(ABC):
|
||||
"""
|
||||
try:
|
||||
msg = input("> ").strip().lower()
|
||||
except EOFError: # Ctrl+D shall be equivalent to the `CLIENT_EXIT` command.
|
||||
except EOFError: # Ctrl+D shall be equivalent to the :const:`CLIENT_EXIT` command.
|
||||
msg = CLIENT_EXIT
|
||||
except KeyboardInterrupt: # Ctrl+C shall simply reset to the input prompt.
|
||||
print()
|
||||
@ -129,11 +140,14 @@ class ControlClient(ABC):
|
||||
|
||||
async def start(self) -> None:
|
||||
"""
|
||||
This method opens the pre-defined connection, performs the server-handshake, and enters the interaction loop.
|
||||
Opens connection, performs handshake, and enters interaction loop.
|
||||
|
||||
An input prompt is presented to the user and any input is sent (encoded) to the connected server.
|
||||
One exception is the :const:`CLIENT_EXIT` command (equivalent to Ctrl+D), which merely closes the connection.
|
||||
|
||||
If the connection can not be established, an error message is printed to `stderr` and the method returns.
|
||||
If the `_connected` flag is set to `False` during the interaction loop, the method returns and prints out a
|
||||
disconnected-message.
|
||||
If either the exit command is issued or the connection to the server is lost during the interaction loop,
|
||||
the method returns and prints out a disconnected-message.
|
||||
"""
|
||||
reader, writer = await self._open_connection(**self._conn_kwargs)
|
||||
if reader is None:
|
||||
@ -146,10 +160,10 @@ class ControlClient(ABC):
|
||||
|
||||
|
||||
class TCPControlClient(ControlClient):
|
||||
"""Task pool control client that expects a TCP socket to be exposed by the control server."""
|
||||
"""Task pool control client for connecting to a :class:`TCPControlServer`."""
|
||||
|
||||
def __init__(self, host: str, port: Union[int, str], **conn_kwargs) -> None:
|
||||
"""In addition to what the base class does, `host` and `port` are expected as non-optional arguments."""
|
||||
"""`host` and `port` are expected as non-optional connection arguments."""
|
||||
self._host = host
|
||||
self._port = port
|
||||
super().__init__(**conn_kwargs)
|
||||
@ -169,10 +183,10 @@ class TCPControlClient(ControlClient):
|
||||
|
||||
|
||||
class UnixControlClient(ControlClient):
|
||||
"""Task pool control client that expects a unix socket to be exposed by the control server."""
|
||||
"""Task pool control client for connecting to a :class:`UnixControlServer`."""
|
||||
|
||||
def __init__(self, socket_path: PathT, **conn_kwargs) -> None:
|
||||
"""In addition to what the base class does, the `socket_path` is expected as a non-optional argument."""
|
||||
"""`socket_path` is expected as a non-optional connection argument."""
|
||||
from asyncio.streams import open_unix_connection
|
||||
self._open_unix_connection = open_unix_connection
|
||||
self._socket_path = Path(socket_path)
|
||||
|
@ -15,7 +15,8 @@ You should have received a copy of the GNU Lesser General Public License along w
|
||||
If not, see <https://www.gnu.org/licenses/>."""
|
||||
|
||||
__doc__ = """
|
||||
This module contains the the definition of the `ControlParser` class used by a control server.
|
||||
Definition of the :class:`ControlParser` used in a
|
||||
:class:`ControlSession <asyncio_taskpool.control.session.ControlSession>`.
|
||||
"""
|
||||
|
||||
|
||||
@ -26,10 +27,13 @@ from inspect import Parameter, getmembers, isfunction, signature
|
||||
from shutil import get_terminal_size
|
||||
from typing import Any, Callable, Container, Dict, Iterable, Set, Type, TypeVar
|
||||
|
||||
from ..constants import CLIENT_INFO, CMD, STREAM_WRITER
|
||||
from ..exceptions import HelpRequested, ParserError
|
||||
from ..helpers import get_first_doc_line, resolve_dotted_path
|
||||
from ..types import ArgsT, CancelCB, CoroutineFunc, EndCB, KwArgsT
|
||||
from ..internals.constants import CLIENT_INFO, CMD, STREAM_WRITER
|
||||
from ..internals.helpers import get_first_doc_line, resolve_dotted_path
|
||||
from ..internals.types import ArgsT, CancelCB, CoroutineFunc, EndCB, KwArgsT
|
||||
|
||||
|
||||
__all__ = ['ControlParser']
|
||||
|
||||
|
||||
FmtCls = TypeVar('FmtCls', bound=Type[HelpFormatter])
|
||||
@ -42,7 +46,7 @@ NAME, PROG, HELP, DESCRIPTION = 'name', 'prog', 'help', 'description'
|
||||
|
||||
class ControlParser(ArgumentParser):
|
||||
"""
|
||||
Subclass of the standard `argparse.ArgumentParser` for remote interaction.
|
||||
Subclass of the standard :code:`argparse.ArgumentParser` for pool control.
|
||||
|
||||
Such a parser is not supposed to ever print to stdout/stderr, but instead direct all messages to a `StreamWriter`
|
||||
instance passed to it during initialization.
|
||||
@ -54,16 +58,18 @@ class ControlParser(ArgumentParser):
|
||||
@staticmethod
|
||||
def help_formatter_factory(terminal_width: int, base_cls: FmtCls = None) -> FmtCls:
|
||||
"""
|
||||
Constructs and returns a subclass of `argparse.HelpFormatter` with a fixed terminal width argument.
|
||||
Constructs and returns a subclass of :class:`argparse.HelpFormatter`
|
||||
|
||||
Although a custom formatter class can be explicitly passed into the `ArgumentParser` constructor, this is not
|
||||
as convenient, when making use of sub-parsers.
|
||||
The formatter class will have the defined `terminal_width`.
|
||||
|
||||
Although a custom formatter class can be explicitly passed into the :class:`ArgumentParser` constructor,
|
||||
this is not as convenient, when making use of sub-parsers.
|
||||
|
||||
Args:
|
||||
terminal_width:
|
||||
The number of columns of the terminal to which to adjust help formatting.
|
||||
base_cls (optional):
|
||||
The base class to use for inheritance. By default `argparse.ArgumentDefaultsHelpFormatter` is used.
|
||||
Base class to use for inheritance. By default :class:`argparse.ArgumentDefaultsHelpFormatter` is used.
|
||||
|
||||
Returns:
|
||||
The subclass of `base_cls` which fixes the constructor's `width` keyword-argument to `terminal_width`.
|
||||
@ -77,21 +83,19 @@ class ControlParser(ArgumentParser):
|
||||
super().__init__(*args, **kwargs)
|
||||
return ClientHelpFormatter
|
||||
|
||||
def __init__(self, stream_writer: StreamWriter, terminal_width: int = None,
|
||||
**kwargs) -> None:
|
||||
def __init__(self, stream_writer: StreamWriter, terminal_width: int = None, **kwargs) -> None:
|
||||
"""
|
||||
Subclass of the `ArgumentParser` geared towards asynchronous interaction with an object "from the outside".
|
||||
|
||||
Allows directing output to a specified writer rather than stdout/stderr and setting terminal width explicitly.
|
||||
Sets some internal attributes in addition to the base class.
|
||||
|
||||
Args:
|
||||
stream_writer:
|
||||
The instance of the `asyncio.StreamWriter` to use for message output.
|
||||
The instance of the :class:`asyncio.StreamWriter` to use for message output.
|
||||
terminal_width (optional):
|
||||
The terminal width to use for all message formatting. Defaults to `shutil.get_terminal_size().columns`.
|
||||
The terminal width to use for all message formatting. By default the :code:`columns` attribute from
|
||||
:func:`shutil.get_terminal_size` is taken.
|
||||
**kwargs(optional):
|
||||
Passed to the parent class constructor. The exception is the `formatter_class` parameter: Even if a
|
||||
class is specified, it will always be subclassed in the `help_formatter_factory`.
|
||||
class is specified, it will always be subclassed in the :meth:`help_formatter_factory`.
|
||||
Also, by default, `exit_on_error` is set to `False` (as opposed to how the parent class handles it).
|
||||
"""
|
||||
self._stream_writer: StreamWriter = stream_writer
|
||||
@ -105,12 +109,12 @@ class ControlParser(ArgumentParser):
|
||||
def add_function_command(self, function: Callable, omit_params: Container[str] = OMIT_PARAMS_DEFAULT,
|
||||
**subparser_kwargs) -> 'ControlParser':
|
||||
"""
|
||||
Takes a function along with its parameters and adds a corresponding (sub-)command to the parser.
|
||||
Takes a function and adds a corresponding (sub-)command to the parser.
|
||||
|
||||
The `add_subparsers` method must have been called prior to this.
|
||||
The :meth:`add_subparsers` method must have been called prior to this.
|
||||
|
||||
NOTE: Currently, only a limited spectrum of parameters can be accurately converted to a parser argument.
|
||||
This method works correctly with any public method of the `SimpleTaskPool` class.
|
||||
NOTE: Currently, only a limited spectrum of parameters can be accurately converted to parser arguments.
|
||||
This method works correctly with any public method of the any task pool class.
|
||||
|
||||
Args:
|
||||
function:
|
||||
@ -118,7 +122,7 @@ class ControlParser(ArgumentParser):
|
||||
omit_params (optional):
|
||||
Names of function parameters not to add as parser arguments.
|
||||
**subparser_kwargs (optional):
|
||||
Passed directly to the `add_parser` method.
|
||||
Passed directly to the :meth:`add_parser` method.
|
||||
|
||||
Returns:
|
||||
The subparser instance created from the function.
|
||||
@ -133,7 +137,7 @@ class ControlParser(ArgumentParser):
|
||||
|
||||
def add_property_command(self, prop: property, cls_name: str = '', **subparser_kwargs) -> 'ControlParser':
|
||||
"""
|
||||
Same as the `add_function_command` method, but for properties.
|
||||
Same as the :meth:`add_function_command` method, but for properties.
|
||||
|
||||
Args:
|
||||
prop:
|
||||
@ -141,7 +145,7 @@ class ControlParser(ArgumentParser):
|
||||
cls_name (optional):
|
||||
Name of the class the property is defined on to appear in the command help text.
|
||||
**subparser_kwargs (optional):
|
||||
Passed directly to the `add_parser` method.
|
||||
Passed directly to the :meth:`add_parser` method.
|
||||
|
||||
Returns:
|
||||
The subparser instance created from the property.
|
||||
@ -164,12 +168,12 @@ class ControlParser(ArgumentParser):
|
||||
def add_class_commands(self, cls: Type, public_only: bool = True, omit_members: Container[str] = (),
|
||||
member_arg_name: str = CMD) -> ParsersDict:
|
||||
"""
|
||||
Takes a class and adds its methods and properties as (sub-)commands to the parser.
|
||||
Adds methods/properties of a class as (sub-)commands to the parser.
|
||||
|
||||
The `add_subparsers` method must have been called prior to this.
|
||||
The :meth:`add_subparsers` method must have been called prior to this.
|
||||
|
||||
NOTE: Currently, only a limited spectrum of function parameters can be accurately converted to parser arguments.
|
||||
This method works correctly with the `SimpleTaskPool` class.
|
||||
This method works correctly with any task pool class.
|
||||
|
||||
Args:
|
||||
cls:
|
||||
@ -181,7 +185,6 @@ class ControlParser(ArgumentParser):
|
||||
member_arg_name (optional):
|
||||
After parsing the arguments, depending on which command was invoked by the user, the corresponding
|
||||
method/property will be stored as an extra argument in the parsed namespace under this attribute name.
|
||||
Defaults to `constants.CMD`.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping class member names to the (sub-)parsers created from them.
|
||||
@ -202,7 +205,7 @@ class ControlParser(ArgumentParser):
|
||||
return parsers
|
||||
|
||||
def add_subparsers(self, *args, **kwargs):
|
||||
"""Adds the subparsers action as an internal attribute before returning it."""
|
||||
"""Adds the subparsers action as an attribute before returning it."""
|
||||
self._commands = super().add_subparsers(*args, **kwargs)
|
||||
return self._commands
|
||||
|
||||
@ -217,28 +220,28 @@ class ControlParser(ArgumentParser):
|
||||
self._print_message(message)
|
||||
|
||||
def error(self, message: str) -> None:
|
||||
"""This just adds the custom `HelpRequested` exception after the parent class' method."""
|
||||
"""Raises the :exc:`ParserError <asyncio_taskpool.exceptions.ParserError>` exception at the end."""
|
||||
super().error(message=message)
|
||||
raise ParserError
|
||||
|
||||
def print_help(self, file=None) -> None:
|
||||
"""This just adds the custom `HelpRequested` exception after the parent class' method."""
|
||||
"""Raises the :exc:`HelpRequested <asyncio_taskpool.exceptions.HelpRequested>` exception at the end."""
|
||||
super().print_help(file)
|
||||
raise HelpRequested
|
||||
|
||||
def add_function_arg(self, parameter: Parameter, **kwargs) -> Action:
|
||||
"""
|
||||
Takes an `inspect.Parameter` of a function and adds a corresponding argument to the parser.
|
||||
Takes an :class:`inspect.Parameter` and adds a corresponding parser argument.
|
||||
|
||||
NOTE: Currently, only a limited spectrum of parameters can be accurately converted to a parser argument.
|
||||
This method works correctly with any parameter of any public method of the `SimpleTaskPool` class.
|
||||
This method works correctly with any parameter of any public method any task pool class.
|
||||
|
||||
Args:
|
||||
parameter: The `inspect.Parameter` object to be converted to a parser argument.
|
||||
**kwargs: Passed to the `add_argument` method of the base class.
|
||||
parameter: The :class:`inspect.Parameter` object to be converted to a parser argument.
|
||||
**kwargs: Passed to the :meth:`add_argument` method of the base class.
|
||||
|
||||
Returns:
|
||||
The `argparse.Action` returned by the `add_argument` method.
|
||||
The :class:`argparse.Action` returned by the :meth:`add_argument` method.
|
||||
"""
|
||||
if parameter.default is Parameter.empty:
|
||||
# A non-optional function parameter should correspond to a positional argument.
|
||||
@ -273,10 +276,10 @@ class ControlParser(ArgumentParser):
|
||||
|
||||
def add_function_args(self, function: Callable, omit: Container[str] = OMIT_PARAMS_DEFAULT) -> None:
|
||||
"""
|
||||
Takes a function reference and adds its parameters as arguments to the parser.
|
||||
Takes a function and adds its parameters as arguments to the parser.
|
||||
|
||||
NOTE: Currently, only a limited spectrum of parameters can be accurately converted to a parser argument.
|
||||
This method works correctly with any public method of the `SimpleTaskPool` class.
|
||||
This method works correctly with any public method of any task pool class.
|
||||
|
||||
Args:
|
||||
function:
|
||||
@ -305,6 +308,16 @@ def _get_arg_type_wrapper(cls: Type) -> Callable[[Any], Any]:
|
||||
|
||||
|
||||
def _get_type_from_annotation(annotation: Type) -> Callable[[Any], Any]:
|
||||
"""
|
||||
Returns a type conversion function based on the `annotation` passed.
|
||||
|
||||
Required to properly convert parsed arguments to the type expected by certain pool methods.
|
||||
Each conversion function is wrapped by `_get_arg_type_wrapper`.
|
||||
|
||||
`Callable`-type annotations give the `resolve_dotted_path` function.
|
||||
`Iterable`- or args/kwargs-type annotations give the `ast.literal_eval` function.
|
||||
Others pass unchanged (but still wrapped with `_get_arg_type_wrapper`).
|
||||
"""
|
||||
if any(annotation is t for t in {CoroutineFunc, EndCB, CancelCB}):
|
||||
annotation = resolve_dotted_path
|
||||
if any(annotation is t for t in {ArgsT, KwArgsT, Iterable[ArgsT], Iterable[KwArgsT]}):
|
||||
|
@ -15,7 +15,7 @@ You should have received a copy of the GNU Lesser General Public License along w
|
||||
If not, see <https://www.gnu.org/licenses/>."""
|
||||
|
||||
__doc__ = """
|
||||
This module contains the task pool control server class definitions.
|
||||
Task pool control server class definitions.
|
||||
"""
|
||||
|
||||
|
||||
@ -28,10 +28,13 @@ from asyncio.tasks import Task, create_task
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
from ..pool import TaskPool, SimpleTaskPool
|
||||
from ..types import ConnectedCallbackT
|
||||
from .client import ControlClient, TCPControlClient, UnixControlClient
|
||||
from .session import ControlSession
|
||||
from ..pool import AnyTaskPoolT
|
||||
from ..internals.types import ConnectedCallbackT, PathT
|
||||
|
||||
|
||||
__all__ = ['ControlServer', 'TCPControlServer', 'UnixControlServer']
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -41,17 +44,52 @@ class ControlServer(ABC):
|
||||
"""
|
||||
Abstract base class for a task pool control server.
|
||||
|
||||
This class acts as a wrapper around an async server instance and initializes a `ControlSession` upon a client
|
||||
connecting to it. The entire interface is defined within that session class.
|
||||
This class acts as a wrapper around an async server instance and initializes a
|
||||
:class:`ControlSession <asyncio_taskpool.control.session.ControlSession>` once a client connects to it.
|
||||
The interface is defined within the session class.
|
||||
"""
|
||||
_client_class = ControlClient
|
||||
|
||||
@classmethod
|
||||
@property
|
||||
def client_class_name(cls) -> str:
|
||||
"""Returns the name of the control client class matching the server class."""
|
||||
"""Returns the name of the matching control client class."""
|
||||
return cls._client_class.__name__
|
||||
|
||||
def __init__(self, pool: AnyTaskPoolT, **server_kwargs) -> None:
|
||||
"""
|
||||
Merely sets internal attributes, but does not start the server yet.
|
||||
The task pool must be passed here and can not be set/changed afterwards. This means a control server is always
|
||||
tied to one specific task pool.
|
||||
|
||||
Args:
|
||||
pool:
|
||||
An instance of a `BaseTaskPool` subclass to tie the server to.
|
||||
**server_kwargs (optional):
|
||||
Keyword arguments that will be passed into the function that starts the server.
|
||||
"""
|
||||
self._pool: AnyTaskPoolT = pool
|
||||
self._server_kwargs = server_kwargs
|
||||
self._server: Optional[AbstractServer] = None
|
||||
|
||||
@property
|
||||
def pool(self) -> AnyTaskPoolT:
|
||||
"""The task pool instance controlled by the server."""
|
||||
return self._pool
|
||||
|
||||
def is_serving(self) -> bool:
|
||||
"""Wrapper around the `asyncio.Server.is_serving` method."""
|
||||
return self._server.is_serving()
|
||||
|
||||
async def _client_connected_cb(self, reader: StreamReader, writer: StreamWriter) -> None:
|
||||
"""
|
||||
The universal client callback that will be passed into the `_get_server_instance` method.
|
||||
Instantiates a control session, performs the client handshake, and enters the session's `listen` loop.
|
||||
"""
|
||||
session = ControlSession(self, reader, writer)
|
||||
await session.client_handshake()
|
||||
await session.listen()
|
||||
|
||||
@abstractmethod
|
||||
async def _get_server_instance(self, client_connected_cb: ConnectedCallbackT, **kwargs) -> AbstractServer:
|
||||
"""
|
||||
@ -74,40 +112,6 @@ class ControlServer(ABC):
|
||||
"""The method to run after the server's `serve_forever` methods ends for whatever reason."""
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, pool: Union[TaskPool, SimpleTaskPool], **server_kwargs) -> None:
|
||||
"""
|
||||
Initializes by merely saving the internal attributes, but without starting the server yet.
|
||||
The task pool must be passed here and can not be set/changed afterwards. This means a control server is always
|
||||
tied to one specific task pool.
|
||||
|
||||
Args:
|
||||
pool:
|
||||
An instance of a `BaseTaskPool` subclass to tie the server to.
|
||||
**server_kwargs (optional):
|
||||
Keyword arguments that will be passed into the function that starts the server.
|
||||
"""
|
||||
self._pool: Union[TaskPool, SimpleTaskPool] = pool
|
||||
self._server_kwargs = server_kwargs
|
||||
self._server: Optional[AbstractServer] = None
|
||||
|
||||
@property
|
||||
def pool(self) -> Union[TaskPool, SimpleTaskPool]:
|
||||
"""Read-only property for accessing the task pool instance controlled by the server."""
|
||||
return self._pool
|
||||
|
||||
def is_serving(self) -> bool:
|
||||
"""Wrapper around the `asyncio.Server.is_serving` method."""
|
||||
return self._server.is_serving()
|
||||
|
||||
async def _client_connected_cb(self, reader: StreamReader, writer: StreamWriter) -> None:
|
||||
"""
|
||||
The universal client callback that will be passed into the `_get_server_instance` method.
|
||||
Instantiates a control session, performs the client handshake, and enters the session's `listen` loop.
|
||||
"""
|
||||
session = ControlSession(self, reader, writer)
|
||||
await session.client_handshake()
|
||||
await session.listen()
|
||||
|
||||
async def _serve_forever(self) -> None:
|
||||
"""
|
||||
To be run as an `asyncio.Task` by the following method.
|
||||
@ -124,9 +128,12 @@ class ControlServer(ABC):
|
||||
|
||||
async def serve_forever(self) -> Task:
|
||||
"""
|
||||
This method actually starts the server and begins listening to client connections on the specified interface.
|
||||
Starts the server and begins listening to client connections.
|
||||
|
||||
It should never block because the serving will be performed in a separate task.
|
||||
|
||||
Returns:
|
||||
The forever serving task. To stop the server, this task should be cancelled.
|
||||
"""
|
||||
log.debug("Starting %s...", self.__class__.__name__)
|
||||
self._server = await self._get_server_instance(self._client_connected_cb, **self._server_kwargs)
|
||||
@ -134,12 +141,13 @@ class ControlServer(ABC):
|
||||
|
||||
|
||||
class TCPControlServer(ControlServer):
|
||||
"""Task pool control server class that exposes a TCP socket for control clients to connect to."""
|
||||
"""Exposes a TCP socket for control clients to connect to."""
|
||||
_client_class = TCPControlClient
|
||||
|
||||
def __init__(self, pool: Union[TaskPool, SimpleTaskPool], **server_kwargs) -> None:
|
||||
self._host = server_kwargs.pop('host')
|
||||
self._port = server_kwargs.pop('port')
|
||||
def __init__(self, pool: AnyTaskPoolT, host: str, port: Union[int, str], **server_kwargs) -> None:
|
||||
"""`host` and `port` are expected as non-optional server arguments."""
|
||||
self._host = host
|
||||
self._port = port
|
||||
super().__init__(pool, **server_kwargs)
|
||||
|
||||
async def _get_server_instance(self, client_connected_cb: ConnectedCallbackT, **kwargs) -> AbstractServer:
|
||||
@ -152,13 +160,14 @@ class TCPControlServer(ControlServer):
|
||||
|
||||
|
||||
class UnixControlServer(ControlServer):
|
||||
"""Task pool control server class that exposes a unix socket for control clients to connect to."""
|
||||
"""Exposes a unix socket for control clients to connect to."""
|
||||
_client_class = UnixControlClient
|
||||
|
||||
def __init__(self, pool: Union[TaskPool, SimpleTaskPool], **server_kwargs) -> None:
|
||||
def __init__(self, pool: AnyTaskPoolT, socket_path: PathT, **server_kwargs) -> None:
|
||||
"""`socket_path` is expected as a non-optional server argument."""
|
||||
from asyncio.streams import start_unix_server
|
||||
self._start_unix_server = start_unix_server
|
||||
self._socket_path = Path(server_kwargs.pop('path'))
|
||||
self._socket_path = Path(socket_path)
|
||||
super().__init__(pool, **server_kwargs)
|
||||
|
||||
async def _get_server_instance(self, client_connected_cb: ConnectedCallbackT, **kwargs) -> AbstractServer:
|
||||
|
@ -15,7 +15,7 @@ You should have received a copy of the GNU Lesser General Public License along w
|
||||
If not, see <https://www.gnu.org/licenses/>."""
|
||||
|
||||
__doc__ = """
|
||||
This module contains the the definition of the `ControlSession` class used by the control server.
|
||||
Definition of the :class:`ControlSession` used by a :class:`ControlServer`.
|
||||
"""
|
||||
|
||||
|
||||
@ -26,30 +26,33 @@ from asyncio.streams import StreamReader, StreamWriter
|
||||
from inspect import isfunction, signature
|
||||
from typing import Callable, Optional, Union, TYPE_CHECKING
|
||||
|
||||
from ..constants import CLIENT_INFO, CMD, CMD_OK, SESSION_MSG_BYTES, STREAM_WRITER
|
||||
from ..exceptions import CommandError, HelpRequested, ParserError
|
||||
from ..helpers import return_or_exception
|
||||
from ..pool import TaskPool, SimpleTaskPool
|
||||
from .parser import ControlParser
|
||||
from ..exceptions import CommandError, HelpRequested, ParserError
|
||||
from ..pool import TaskPool, SimpleTaskPool
|
||||
from ..internals.constants import CLIENT_INFO, CMD, CMD_OK, SESSION_MSG_BYTES, STREAM_WRITER
|
||||
from ..internals.helpers import return_or_exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .server import ControlServer
|
||||
|
||||
|
||||
__all__ = ['ControlSession']
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ControlSession:
|
||||
"""
|
||||
This class defines the API for controlling a task pool instance from the outside.
|
||||
Manages a single control session between a server and a client.
|
||||
|
||||
The commands received from a connected client are translated into method calls on the task pool instance.
|
||||
A subclass of the standard `argparse.ArgumentParser` is used to handle the input read from the stream.
|
||||
A subclass of the standard :class:`argparse.ArgumentParser` is used to handle the input read from the stream.
|
||||
"""
|
||||
|
||||
def __init__(self, server: 'ControlServer', reader: StreamReader, writer: StreamWriter) -> None:
|
||||
"""
|
||||
Instantiation should happen once a client connection to the control server has already been established.
|
||||
Connection to the control server should already been established.
|
||||
|
||||
For more convenient/efficient access, some of the server's properties are saved in separate attributes.
|
||||
The argument parser is _not_ instantiated in the constructor. It requires a bit of client information during
|
||||
@ -57,7 +60,7 @@ class ControlSession:
|
||||
|
||||
Args:
|
||||
server:
|
||||
The instance of a `ControlServer` subclass starting the session.
|
||||
The instance of a :class:`ControlServer` subclass starting the session.
|
||||
reader:
|
||||
The `asyncio.StreamReader` created when a client connected to the server.
|
||||
writer:
|
||||
@ -75,8 +78,9 @@ class ControlSession:
|
||||
Takes a pool method reference, executes it, and writes a response accordingly.
|
||||
|
||||
If the first parameter is named `self`, the method will be called with the `_pool` instance as its first
|
||||
positional argument. If it returns nothing, the response upon successful execution will be `constants.CMD_OK`,
|
||||
otherwise the response written to the stream will be its return value (as an encoded string).
|
||||
positional argument.
|
||||
If it returns nothing, the response upon successful execution will be :const:`constants.CMD_OK`, otherwise the
|
||||
response written to the stream will be its return value (as an encoded string).
|
||||
|
||||
Args:
|
||||
prop:
|
||||
@ -108,7 +112,7 @@ class ControlSession:
|
||||
The reference to the property defined on the `_pool` instance's class.
|
||||
**kwargs (optional):
|
||||
If not empty, the property setter is executed and the keyword arguments are passed along to it; the
|
||||
response upon successful execution will be `constants.CMD_OK`. Otherwise the property getter is
|
||||
response upon successful execution will be :const:`constants.CMD_OK`. Otherwise the property getter is
|
||||
executed and the response written to the stream will be its return value (as an encoded string).
|
||||
"""
|
||||
if kwargs:
|
||||
@ -121,9 +125,10 @@ class ControlSession:
|
||||
|
||||
async def client_handshake(self) -> None:
|
||||
"""
|
||||
This method must be invoked before starting any other client interaction.
|
||||
Must be invoked before starting any other client interaction.
|
||||
|
||||
Client info is retrieved, server info is sent back, and the `ControlParser` is initialized and configured.
|
||||
Client info is retrieved, server info is sent back, and the
|
||||
:class:`ControlParser <asyncio_taskpool.control.parser.ControlParser>` is set up.
|
||||
"""
|
||||
client_info = json.loads((await self._reader.read(SESSION_MSG_BYTES)).decode().strip())
|
||||
log.debug("%s connected", self._client_class_name)
|
||||
@ -144,9 +149,9 @@ class ControlSession:
|
||||
"""
|
||||
Takes a message from the client and attempts to parse it.
|
||||
|
||||
If a parsing error occurs, it is returned to the client. If the `HelpRequested` exception was raised by the
|
||||
`ControlParser`, nothing else happens. Otherwise, the appropriate `_exec...` method is called with the entire
|
||||
dictionary of keyword-arguments returned by the `ControlParser` passed into it.
|
||||
If a parsing error occurs, it is returned to the client. If the :exc:`HelpRequested` exception was raised by the
|
||||
:class:`ControlParser`, nothing else happens. Otherwise, the appropriate `_exec...` method is called with the
|
||||
entire dictionary of keyword-arguments returned by the :class:`ControlParser` passed into it.
|
||||
|
||||
Args:
|
||||
msg: The non-empty string read from the client stream.
|
||||
@ -170,9 +175,10 @@ class ControlSession:
|
||||
|
||||
async def listen(self) -> None:
|
||||
"""
|
||||
Enters the main control loop that only ends if either the server or the client disconnect.
|
||||
Enters the main control loop listening to client input.
|
||||
|
||||
Messages from the client are read and passed into the `_parse_command` method, which handles the rest.
|
||||
This method only returns if either the server or the client disconnect.
|
||||
Messages from the client are read, parsed, and turned into pool commands (if possible).
|
||||
This method should be called, when the client connection was established and the handshake was successful.
|
||||
It will obviously block indefinitely.
|
||||
"""
|
||||
|
Reference in New Issue
Block a user