From 9167b76283feda7064626ecb368613538f3911b4 Mon Sep 17 00:00:00 2001 From: kmochocki <kmochocki@syncad.com> Date: Fri, 20 Sep 2024 16:23:55 +0000 Subject: [PATCH] Move all apis to one directory in _interfaces --- .gitlab-ci.yml | 15 +- beekeepy/beekeepy/__init__.py | 25 +- beekeepy/beekeepy/_apis/__init__.py | 30 ++ beekeepy/beekeepy/_apis/abc/__init__.py | 33 +++ .../{_remote_handle => _apis}/abc/api.py | 15 +- .../abc/api_collection.py | 11 +- beekeepy/beekeepy/_apis/abc/sendable.py | 21 ++ .../api => _apis/abc}/session_holder.py | 7 +- .../beekeepy/_apis/app_status_api/__init__.py | 19 ++ .../_apis/app_status_api/api_collection.py | 32 +++ .../_apis/app_status_api/async_api.py | 12 + .../beekeepy/_apis/app_status_api/sync_api.py | 12 + .../api => _apis}/apply_session_token.py | 3 +- .../beekeepy/_apis/beekeeper_api/__init__.py | 14 + .../_apis/beekeeper_api/api_collection.py | 32 +++ .../api => _apis/beekeeper_api}/async_api.py | 21 +- .../beekeeper_api}/beekeeper_api_commons.py | 4 +- .../api => _apis/beekeeper_api}/sync_api.py | 22 +- beekeepy/beekeepy/_communication/__init__.py | 28 ++ .../_communication/abc/communicator.py | 8 +- .../abc/http_server_observer.py | 17 -- .../abc/notification_handler.py | 21 -- .../beekeepy/_communication/abc/overseer.py | 11 +- beekeepy/beekeepy/_communication/abc/rules.py | 2 +- .../_communication/aiohttp_communicator.py | 4 +- .../appbase_notification_handler.py | 40 --- .../beekeepy/_communication/async_server.py | 104 ------- .../_communication/httpx_communicator.py | 4 +- .../_communication/notification_decorator.py | 43 --- .../_communication/request_communicator.py | 4 +- beekeepy/beekeepy/_communication/settings.py | 2 +- .../universal_notification_server.py | 87 ------ .../{_interface => _communication}/url.py | 11 + beekeepy/beekeepy/_executable/__init__.py | 24 +- beekeepy/beekeepy/_executable/abc/__init__.py | 16 ++ .../beekeepy/_executable/abc/arguments.py | 144 ++++++++++ .../{_interface => _executable/abc}/config.py | 50 +++- .../_executable/{ => abc}/executable.py | 109 ++++++-- .../beekeepy/_executable/{ => abc}/streams.py | 2 +- .../_executable/arguments/arguments.py | 64 ----- .../{arguments => }/beekeeper_arguments.py | 9 +- .../beekeepy/_executable/beekeeper_config.py | 5 +- .../_executable/beekeeper_executable.py | 43 +-- beekeepy/beekeepy/_executable/defaults.py | 3 +- beekeepy/beekeepy/_interface/__init__.py | 24 ++ beekeepy/beekeepy/_interface/abc/__init__.py | 23 ++ .../_interface/abc/asynchronous/beekeeper.py | 18 +- .../_interface/abc/asynchronous/session.py | 2 +- .../_interface/abc/asynchronous/wallet.py | 2 +- .../beekeepy/_interface/abc/packed_object.py | 19 +- .../_interface/abc/synchronous/beekeeper.py | 18 +- .../_interface/abc/synchronous/session.py | 2 +- .../_interface/abc/synchronous/wallet.py | 2 +- .../_interface/asynchronous/beekeeper.py | 41 +-- .../_interface/asynchronous/session.py | 9 +- .../_interface/asynchronous/wallet.py | 11 +- beekeepy/beekeepy/_interface/settings.py | 7 + .../_interface/synchronous/beekeeper.py | 41 +-- .../_interface/synchronous/session.py | 9 +- .../beekeepy/_interface/synchronous/wallet.py | 9 +- .../beekeepy/_interface/wallets_common.py | 21 +- beekeepy/beekeepy/_remote_handle/__init__.py | 30 ++ .../_remote_handle/{ => abc}/batch_handle.py | 12 +- .../beekeepy/_remote_handle/abc/handle.py | 44 +-- .../beekeepy/_remote_handle/api/__init__.py | 6 - .../_remote_handle/api/api_collection.py | 39 --- .../_remote_handle/app_status_probe.py | 28 ++ beekeepy/beekeepy/_remote_handle/beekeeper.py | 35 ++- beekeepy/beekeepy/_remote_handle/settings.py | 14 +- .../beekeepy/_runnable_handle/__init__.py | 21 +- .../beekeepy/_runnable_handle/beekeeper.py | 248 ++++------------- .../_runnable_handle/beekeeper_callbacks.py | 42 --- .../beekeeper_notification_handler.py | 70 ----- .../beekeepy/_runnable_handle/match_ports.py | 72 +++++ .../notification_handler_base.py | 29 -- .../_runnable_handle/runnable_handle.py | 256 ++++++++++++++++++ .../beekeepy/_runnable_handle/settings.py | 18 +- .../arguments => _utilities}/__init__.py | 0 .../build_json_rpc_call.py | 0 .../{_interface => _utilities}/context.py | 0 .../context_settings_updater.py | 4 +- .../{_interface => _utilities}/delay_guard.py | 2 +- .../error_logger.py | 4 +- .../{_interface => _utilities}/key_pair.py | 0 .../_sanitize.py => _utilities/sanitize.py} | 0 .../settings_holder.py | 2 +- .../state_invalidator.py | 0 .../{_interface => _utilities}/stopwatch.py | 2 +- .../suppress_api_not_found.py} | 2 +- beekeepy/beekeepy/exceptions/__init__.py | 18 +- beekeepy/beekeepy/exceptions/base.py | 10 +- beekeepy/beekeepy/exceptions/common.py | 18 +- beekeepy/beekeepy/exceptions/executable.py | 23 ++ beekeepy/beekeepy/exceptions/overseer.py | 4 +- beekeepy/beekeepy/handle/remote.py | 24 +- beekeepy/beekeepy/handle/runnable.py | 23 +- beekeepy/beekeepy/interfaces.py | 26 +- beekeepy/poetry.lock | 37 ++- beekeepy/pyproject.toml | 6 +- hive | 2 +- .../handle/api_tests/test_api_close.py | 12 +- .../api_tests/test_api_close_session.py | 6 +- .../api_tests/test_api_create_session.py | 8 +- .../handle/api_tests/test_api_set_timeout.py | 4 +- .../beekeepy_test/handle/basic/test_wallet.py | 4 +- .../patterns/config.ini | 2 +- .../patterns/generate_help_pattern.py | 5 +- .../patterns/help_pattern.txt | 6 +- .../application_options/test_backtrace.py | 9 +- .../test_default_values.py | 1 - .../test_export_keys_wallet.py | 4 +- .../application_options/test_log_json_rpc.py | 2 +- .../test_notifications_endpoint.py | 37 --- .../test_unlock_timeout.py | 4 +- .../application_options/test_wallet_dir.py | 2 +- .../test_webserver_http_endpoint.py | 17 +- .../test_webserver_thread_pool_size.py | 2 +- .../handle/commandline/conftest.py | 6 +- tests/beekeepy_test/handle/conftest.py | 4 +- .../handle/storage/test_storage.py | 59 +--- .../handle/various/test_blocking_unlock.py | 16 +- tests/beekeepy_test/interface/test_setup.py | 16 +- .../interface/test_standalone_beekeeper.py | 6 +- tests/conftest.py | 18 +- .../beekeepy/account_credentials.py | 2 +- .../local_tools/beekeepy/network.py | 11 +- tests/local-tools/poetry.lock | 50 +++- tests/local-tools/pyproject.toml | 1 + 128 files changed, 1600 insertions(+), 1326 deletions(-) create mode 100644 beekeepy/beekeepy/_apis/__init__.py create mode 100644 beekeepy/beekeepy/_apis/abc/__init__.py rename beekeepy/beekeepy/{_remote_handle => _apis}/abc/api.py (93%) rename beekeepy/beekeepy/{_remote_handle => _apis}/abc/api_collection.py (52%) create mode 100644 beekeepy/beekeepy/_apis/abc/sendable.py rename beekeepy/beekeepy/{_remote_handle/api => _apis/abc}/session_holder.py (88%) create mode 100644 beekeepy/beekeepy/_apis/app_status_api/__init__.py create mode 100644 beekeepy/beekeepy/_apis/app_status_api/api_collection.py create mode 100644 beekeepy/beekeepy/_apis/app_status_api/async_api.py create mode 100644 beekeepy/beekeepy/_apis/app_status_api/sync_api.py rename beekeepy/beekeepy/{_remote_handle/api => _apis}/apply_session_token.py (76%) create mode 100644 beekeepy/beekeepy/_apis/beekeeper_api/__init__.py create mode 100644 beekeepy/beekeepy/_apis/beekeeper_api/api_collection.py rename beekeepy/beekeepy/{_remote_handle/api => _apis/beekeeper_api}/async_api.py (90%) rename beekeepy/beekeepy/{_remote_handle/api => _apis/beekeeper_api}/beekeeper_api_commons.py (86%) rename beekeepy/beekeepy/{_remote_handle/api => _apis/beekeeper_api}/sync_api.py (81%) delete mode 100644 beekeepy/beekeepy/_communication/abc/http_server_observer.py delete mode 100644 beekeepy/beekeepy/_communication/abc/notification_handler.py delete mode 100644 beekeepy/beekeepy/_communication/appbase_notification_handler.py delete mode 100644 beekeepy/beekeepy/_communication/async_server.py delete mode 100644 beekeepy/beekeepy/_communication/notification_decorator.py delete mode 100644 beekeepy/beekeepy/_communication/universal_notification_server.py rename beekeepy/beekeepy/{_interface => _communication}/url.py (88%) create mode 100644 beekeepy/beekeepy/_executable/abc/__init__.py create mode 100644 beekeepy/beekeepy/_executable/abc/arguments.py rename beekeepy/beekeepy/{_interface => _executable/abc}/config.py (59%) rename beekeepy/beekeepy/_executable/{ => abc}/executable.py (62%) rename beekeepy/beekeepy/_executable/{ => abc}/streams.py (98%) delete mode 100644 beekeepy/beekeepy/_executable/arguments/arguments.py rename beekeepy/beekeepy/_executable/{arguments => }/beekeeper_arguments.py (82%) create mode 100644 beekeepy/beekeepy/_interface/settings.py rename beekeepy/beekeepy/_remote_handle/{ => abc}/batch_handle.py (94%) delete mode 100644 beekeepy/beekeepy/_remote_handle/api/__init__.py delete mode 100644 beekeepy/beekeepy/_remote_handle/api/api_collection.py create mode 100644 beekeepy/beekeepy/_remote_handle/app_status_probe.py delete mode 100644 beekeepy/beekeepy/_runnable_handle/beekeeper_callbacks.py delete mode 100644 beekeepy/beekeepy/_runnable_handle/beekeeper_notification_handler.py create mode 100644 beekeepy/beekeepy/_runnable_handle/match_ports.py delete mode 100644 beekeepy/beekeepy/_runnable_handle/notification_handler_base.py create mode 100644 beekeepy/beekeepy/_runnable_handle/runnable_handle.py rename beekeepy/beekeepy/{_executable/arguments => _utilities}/__init__.py (100%) rename beekeepy/beekeepy/{_remote_handle => _utilities}/build_json_rpc_call.py (100%) rename beekeepy/beekeepy/{_interface => _utilities}/context.py (100%) rename beekeepy/beekeepy/{_interface => _utilities}/context_settings_updater.py (93%) rename beekeepy/beekeepy/{_interface => _utilities}/delay_guard.py (97%) rename beekeepy/beekeepy/{_interface => _utilities}/error_logger.py (93%) rename beekeepy/beekeepy/{_interface => _utilities}/key_pair.py (100%) rename beekeepy/beekeepy/{_interface/_sanitize.py => _utilities/sanitize.py} (100%) rename beekeepy/beekeepy/{_interface => _utilities}/settings_holder.py (97%) rename beekeepy/beekeepy/{_interface => _utilities}/state_invalidator.py (100%) rename beekeepy/beekeepy/{_interface => _utilities}/stopwatch.py (95%) rename beekeepy/beekeepy/{_interface/_suppress_api_not_found.py => _utilities/suppress_api_not_found.py} (96%) create mode 100644 beekeepy/beekeepy/exceptions/executable.py delete mode 100644 tests/beekeepy_test/handle/commandline/application_options/test_notifications_endpoint.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5daaec37..36922925 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,7 +33,7 @@ variables: include: - project: 'hive/hive' # This has to be the same as the commit checked out in the submodule - ref: ed702f86d3a1ae567b2d2e1d0d7240a187223964 + ref: 40e6bc384b63fe116f370153a20c15a46888011f file: '/scripts/ci-helpers/prepare_data_image_job.yml' - project: 'hive/common-ci-configuration' # This should be the same version of Common CI defined in /hive/scripts/ci-helpers/prepare_data_image_job.yml @@ -96,19 +96,6 @@ prepare_hived_image: - public-runner-docker - hived-for-tests -prepare_hived_data: - extends: .prepare_hived_data_5m - needs: - - job: prepare_hived_image - artifacts: true - stage: build - variables: - SUBMODULE_DIR: "$CI_PROJECT_DIR/hive" - BLOCK_LOG_SOURCE_DIR: $BLOCK_LOG_SOURCE_DIR_5M - CONFIG_INI_SOURCE: "$CI_PROJECT_DIR/hive/docker/config_5M.ini" - tags: - - data-cache-storage - build_beekeepy_wheel: stage: beekeepy extends: .build_wheel_template diff --git a/beekeepy/beekeepy/__init__.py b/beekeepy/beekeepy/__init__.py index e416f596..b5402dc7 100644 --- a/beekeepy/beekeepy/__init__.py +++ b/beekeepy/beekeepy/__init__.py @@ -1,16 +1,19 @@ from __future__ import annotations -from beekeepy._interface.abc.asynchronous.beekeeper import Beekeeper as AsyncBeekeeper -from beekeepy._interface.abc.asynchronous.session import Session as AsyncSession -from beekeepy._interface.abc.asynchronous.wallet import UnlockedWallet as AsyncUnlockedWallet -from beekeepy._interface.abc.asynchronous.wallet import Wallet as AsyncWallet -from beekeepy._interface.abc.packed_object import PackedAsyncBeekeeper, PackedSyncBeekeeper -from beekeepy._interface.abc.synchronous.beekeeper import Beekeeper -from beekeepy._interface.abc.synchronous.session import Session -from beekeepy._interface.abc.synchronous.wallet import UnlockedWallet, Wallet -from beekeepy._remote_handle.settings import Settings as RemoteHandleSettings -from beekeepy._runnable_handle.close_already_running_beekeeper import close_already_running_beekeeper -from beekeepy._runnable_handle.settings import Settings +from beekeepy._interface import InterfaceSettings as Settings +from beekeepy._interface.abc import ( + AsyncBeekeeper, + AsyncSession, + AsyncUnlockedWallet, + AsyncWallet, + Beekeeper, + PackedAsyncBeekeeper, + PackedSyncBeekeeper, + Session, + UnlockedWallet, + Wallet, +) +from beekeepy._runnable_handle import close_already_running_beekeeper __all__ = [ "AsyncBeekeeper", diff --git a/beekeepy/beekeepy/_apis/__init__.py b/beekeepy/beekeepy/_apis/__init__.py new file mode 100644 index 00000000..c9d688e0 --- /dev/null +++ b/beekeepy/beekeepy/_apis/__init__.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from beekeepy._apis import abc +from beekeepy._apis.app_status_api import ( + AppStatusProbeAsyncApiCollection, + AppStatusProbeSyncApiCollection, + AsyncAppStatusApi, + SyncAppStatusApi, +) +from beekeepy._apis.apply_session_token import async_apply_session_token, sync_apply_session_token +from beekeepy._apis.beekeeper_api import ( + AsyncBeekeeperApi, + BeekeeperAsyncApiCollection, + BeekeeperSyncApiCollection, + SyncBeekeeperApi, +) + +__all__ = [ + "abc", + "AppStatusProbeAsyncApiCollection", + "AppStatusProbeSyncApiCollection", + "AsyncAppStatusApi", + "SyncAppStatusApi", + "SyncBeekeeperApi", + "AsyncBeekeeperApi", + "BeekeeperSyncApiCollection", + "BeekeeperAsyncApiCollection", + "sync_apply_session_token", + "async_apply_session_token", +] diff --git a/beekeepy/beekeepy/_apis/abc/__init__.py b/beekeepy/beekeepy/_apis/abc/__init__.py new file mode 100644 index 00000000..558e1c1c --- /dev/null +++ b/beekeepy/beekeepy/_apis/abc/__init__.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from beekeepy._apis.abc.api import ( + AbstractApi, + AbstractAsyncApi, + AbstractSyncApi, + ApiArgumentSerialization, + ApiArgumentsToSerialize, + HandleT, + RegisteredApisT, +) +from beekeepy._apis.abc.api_collection import AbstractAsyncApiCollection, AbstractSyncApiCollection +from beekeepy._apis.abc.sendable import ( + AsyncSendable, + SyncSendable, +) +from beekeepy._apis.abc.session_holder import AsyncSessionHolder, SyncSessionHolder + +__all__ = [ + "AbstractApi", + "AbstractAsyncApi", + "AbstractAsyncApiCollection", + "AbstractSyncApi", + "AbstractSyncApiCollection", + "ApiArgumentSerialization", + "ApiArgumentsToSerialize", + "AsyncSendable", + "AsyncSessionHolder", + "HandleT", + "RegisteredApisT", + "SyncSendable", + "SyncSessionHolder", +] diff --git a/beekeepy/beekeepy/_remote_handle/abc/api.py b/beekeepy/beekeepy/_apis/abc/api.py similarity index 93% rename from beekeepy/beekeepy/_remote_handle/abc/api.py rename to beekeepy/beekeepy/_apis/abc/api.py index af7bfc9e..4dcbd68c 100644 --- a/beekeepy/beekeepy/_remote_handle/abc/api.py +++ b/beekeepy/beekeepy/_apis/abc/api.py @@ -13,16 +13,11 @@ from typing import ( ClassVar, Generic, ParamSpec, - TypeAlias, TypeVar, get_type_hints, ) -from beekeepy._remote_handle.abc.handle import ( - AbstractAsyncHandle, - AbstractSyncHandle, -) -from beekeepy._remote_handle.batch_handle import AsyncBatchHandle, SyncBatchHandle +from beekeepy._apis.abc.sendable import AsyncSendable, SyncSendable from schemas._preconfigured_base_model import PreconfiguredBaseModel from schemas.fields.serializable import Serializable from schemas.operations.representations.legacy_representation import LegacyRepresentation @@ -34,9 +29,7 @@ if TYPE_CHECKING: P = ParamSpec("P") -SyncHandleT: TypeAlias = AbstractSyncHandle[Any] | SyncBatchHandle[Any] -AsyncHandleT: TypeAlias = AbstractAsyncHandle[Any] | AsyncBatchHandle[Any] -HandleT = TypeVar("HandleT", bound=SyncHandleT | AsyncHandleT) +HandleT = TypeVar("HandleT", bound=SyncSendable | AsyncSendable) RegisteredApisT = defaultdict[bool, defaultdict[str, set[str]]] ApiArgumentsToSerialize = tuple[tuple[Any, ...], dict[str, Any]] @@ -121,7 +114,7 @@ class AbstractApi(ABC, Generic[HandleT]): self._owner = owner -class AbstractSyncApi(AbstractApi[SyncHandleT]): +class AbstractSyncApi(AbstractApi[SyncSendable]): """Base class for all apis, that provides synchronous endpoints.""" def _additional_arguments_actions( @@ -153,7 +146,7 @@ class AbstractSyncApi(AbstractApi[SyncHandleT]): return impl # type: ignore[return-value] -class AbstractAsyncApi(AbstractApi[AsyncHandleT]): +class AbstractAsyncApi(AbstractApi[AsyncSendable]): """Base class for all apis, that provides asynchronous endpoints.""" async def _additional_arguments_actions( diff --git a/beekeepy/beekeepy/_remote_handle/abc/api_collection.py b/beekeepy/beekeepy/_apis/abc/api_collection.py similarity index 52% rename from beekeepy/beekeepy/_remote_handle/abc/api_collection.py rename to beekeepy/beekeepy/_apis/abc/api_collection.py index d8836883..8838e34b 100644 --- a/beekeepy/beekeepy/_remote_handle/abc/api_collection.py +++ b/beekeepy/beekeepy/_apis/abc/api_collection.py @@ -2,7 +2,8 @@ from __future__ import annotations from typing import Generic -from beekeepy._remote_handle.abc.api import AsyncHandleT, HandleT, SyncHandleT +from beekeepy._apis.abc.api import HandleT +from beekeepy._apis.abc.sendable import AsyncSendable, SyncSendable class AbstractApiCollection(Generic[HandleT]): @@ -12,15 +13,15 @@ class AbstractApiCollection(Generic[HandleT]): self._owner = owner -class AbstractAsyncApiCollection(AbstractApiCollection[AsyncHandleT]): +class AbstractAsyncApiCollection(AbstractApiCollection[AsyncSendable]): """Base class for Async Api Collections.""" - def __init__(self, owner: AsyncHandleT) -> None: + def __init__(self, owner: AsyncSendable) -> None: super().__init__(owner) -class AbstractSyncApiCollection(AbstractApiCollection[SyncHandleT]): +class AbstractSyncApiCollection(AbstractApiCollection[SyncSendable]): """Base class for Sync Api Collections.""" - def __init__(self, owner: SyncHandleT) -> None: + def __init__(self, owner: SyncSendable) -> None: super().__init__(owner) diff --git a/beekeepy/beekeepy/_apis/abc/sendable.py b/beekeepy/beekeepy/_apis/abc/sendable.py new file mode 100644 index 00000000..3f2e5ac1 --- /dev/null +++ b/beekeepy/beekeepy/_apis/abc/sendable.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from schemas.jsonrpc import ExpectResultT, JSONRPCResult + + +class SyncSendable(ABC): + @abstractmethod + def _send( + self, *, endpoint: str, params: str, expected_type: type[ExpectResultT] + ) -> JSONRPCResult[ExpectResultT]: ... + + +class AsyncSendable(ABC): + @abstractmethod + async def _async_send( + self, *, endpoint: str, params: str, expected_type: type[ExpectResultT] + ) -> JSONRPCResult[ExpectResultT]: ... diff --git a/beekeepy/beekeepy/_remote_handle/api/session_holder.py b/beekeepy/beekeepy/_apis/abc/session_holder.py similarity index 88% rename from beekeepy/beekeepy/_remote_handle/api/session_holder.py rename to beekeepy/beekeepy/_apis/abc/session_holder.py index 2c5b3d12..9e314b8b 100644 --- a/beekeepy/beekeepy/_remote_handle/api/session_holder.py +++ b/beekeepy/beekeepy/_apis/abc/session_holder.py @@ -1,8 +1,9 @@ from __future__ import annotations -from abc import abstractmethod +from abc import ABC, abstractmethod from typing import Any +from beekeepy._apis.abc.sendable import AsyncSendable, SyncSendable from schemas.apis.beekeeper_api import CreateSession __all__ = ["SyncSessionHolder", "AsyncSessionHolder"] @@ -37,7 +38,7 @@ class SessionHolder: return self.__session -class SyncSessionHolder(SessionHolder): +class SyncSessionHolder(SyncSendable, SessionHolder, ABC): @abstractmethod def _acquire_session_token(self) -> str: ... @@ -48,7 +49,7 @@ class SyncSessionHolder(SessionHolder): return self._check_and_return_session() -class AsyncSessionHolder(SessionHolder): +class AsyncSessionHolder(AsyncSendable, SessionHolder, ABC): @abstractmethod async def _acquire_session_token(self) -> str: ... diff --git a/beekeepy/beekeepy/_apis/app_status_api/__init__.py b/beekeepy/beekeepy/_apis/app_status_api/__init__.py new file mode 100644 index 00000000..58d93619 --- /dev/null +++ b/beekeepy/beekeepy/_apis/app_status_api/__init__.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from beekeepy._apis.app_status_api.api_collection import ( + AppStatusProbeAsyncApiCollection, + AppStatusProbeSyncApiCollection, +) +from beekeepy._apis.app_status_api.async_api import ( + AppStatusApi as AsyncAppStatusApi, +) +from beekeepy._apis.app_status_api.sync_api import ( + AppStatusApi as SyncAppStatusApi, +) + +__all__ = [ + "AsyncAppStatusApi", + "SyncAppStatusApi", + "AppStatusProbeAsyncApiCollection", + "AppStatusProbeSyncApiCollection", +] diff --git a/beekeepy/beekeepy/_apis/app_status_api/api_collection.py b/beekeepy/beekeepy/_apis/app_status_api/api_collection.py new file mode 100644 index 00000000..a61fc45d --- /dev/null +++ b/beekeepy/beekeepy/_apis/app_status_api/api_collection.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from beekeepy._apis.abc.api_collection import AbstractAsyncApiCollection, AbstractSyncApiCollection +from beekeepy._apis.app_status_api.async_api import AppStatusApi as AsyncAppStatusApi +from beekeepy._apis.app_status_api.sync_api import AppStatusApi as SyncAppStatusApi + +if TYPE_CHECKING: + from beekeepy._apis.abc.sendable import AsyncSendable, SyncSendable + + +class AppStatusProbeSyncApiCollection(AbstractSyncApiCollection): + """Beekeepers collection of available apis in async version.""" + + _owner: SyncSendable + + def __init__(self, owner: SyncSendable) -> None: + super().__init__(owner) + self.app_status = SyncAppStatusApi(owner=self._owner) + self.app_status_api = self.app_status + + +class AppStatusProbeAsyncApiCollection(AbstractAsyncApiCollection): + """Beekeepers collection of available apis in async version.""" + + _owner: AsyncSendable + + def __init__(self, owner: AsyncSendable) -> None: + super().__init__(owner) + self.app_status = AsyncAppStatusApi(owner=self._owner) + self.app_status_api = self.app_status diff --git a/beekeepy/beekeepy/_apis/app_status_api/async_api.py b/beekeepy/beekeepy/_apis/app_status_api/async_api.py new file mode 100644 index 00000000..b6c36b7f --- /dev/null +++ b/beekeepy/beekeepy/_apis/app_status_api/async_api.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from beekeepy._apis.abc.api import AbstractAsyncApi +from schemas.apis import app_status_api # noqa: TCH001 + + +class AppStatusApi(AbstractAsyncApi): + api = AbstractAsyncApi._endpoint + + @api + async def get_app_status(self) -> app_status_api.GetAppStatus: + raise NotImplementedError diff --git a/beekeepy/beekeepy/_apis/app_status_api/sync_api.py b/beekeepy/beekeepy/_apis/app_status_api/sync_api.py new file mode 100644 index 00000000..50633a60 --- /dev/null +++ b/beekeepy/beekeepy/_apis/app_status_api/sync_api.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from beekeepy._apis.abc.api import AbstractSyncApi +from schemas.apis import app_status_api # noqa: TCH001 + + +class AppStatusApi(AbstractSyncApi): + api = AbstractSyncApi._endpoint + + @api + def get_app_status(self) -> app_status_api.GetAppStatus: + raise NotImplementedError diff --git a/beekeepy/beekeepy/_remote_handle/api/apply_session_token.py b/beekeepy/beekeepy/_apis/apply_session_token.py similarity index 76% rename from beekeepy/beekeepy/_remote_handle/api/apply_session_token.py rename to beekeepy/beekeepy/_apis/apply_session_token.py index e7ed3274..a440a918 100644 --- a/beekeepy/beekeepy/_remote_handle/api/apply_session_token.py +++ b/beekeepy/beekeepy/_apis/apply_session_token.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: - from beekeepy._remote_handle.abc.api import ApiArgumentsToSerialize - from beekeepy._remote_handle.api.session_holder import AsyncSessionHolder, SyncSessionHolder + from beekeepy._apis.abc import ApiArgumentsToSerialize, AsyncSessionHolder, SyncSessionHolder def sync_apply_session_token(owner: SyncSessionHolder, arguments: ApiArgumentsToSerialize) -> ApiArgumentsToSerialize: diff --git a/beekeepy/beekeepy/_apis/beekeeper_api/__init__.py b/beekeepy/beekeepy/_apis/beekeeper_api/__init__.py new file mode 100644 index 00000000..dca7d2cf --- /dev/null +++ b/beekeepy/beekeepy/_apis/beekeeper_api/__init__.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from beekeepy._apis.beekeeper_api.api_collection import ( + BeekeeperAsyncApiCollection, + BeekeeperSyncApiCollection, +) +from beekeepy._apis.beekeeper_api.async_api import ( + BeekeeperApi as AsyncBeekeeperApi, +) +from beekeepy._apis.beekeeper_api.sync_api import ( + BeekeeperApi as SyncBeekeeperApi, +) + +__all__ = ["AsyncBeekeeperApi", "SyncBeekeeperApi", "BeekeeperAsyncApiCollection", "BeekeeperSyncApiCollection"] diff --git a/beekeepy/beekeepy/_apis/beekeeper_api/api_collection.py b/beekeepy/beekeepy/_apis/beekeeper_api/api_collection.py new file mode 100644 index 00000000..26744fc9 --- /dev/null +++ b/beekeepy/beekeepy/_apis/beekeeper_api/api_collection.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from beekeepy._apis.app_status_api import AppStatusProbeAsyncApiCollection, AppStatusProbeSyncApiCollection +from beekeepy._apis.beekeeper_api.async_api import BeekeeperApi as AsyncBeekeeperApi +from beekeepy._apis.beekeeper_api.sync_api import BeekeeperApi as SyncBeekeeperApi + +if TYPE_CHECKING: + from beekeepy._apis.abc import AsyncSessionHolder, SyncSessionHolder + + +class BeekeeperAsyncApiCollection(AppStatusProbeAsyncApiCollection): + """Beekeepers collection of available apis in async version.""" + + _owner: AsyncSessionHolder + + def __init__(self, owner: AsyncSessionHolder) -> None: + super().__init__(owner) + self.beekeeper = AsyncBeekeeperApi(owner=self._owner) + self.beekeeper_api = self.beekeeper + + +class BeekeeperSyncApiCollection(AppStatusProbeSyncApiCollection): + """Beekeepers collection of available apis in async version.""" + + _owner: SyncSessionHolder + + def __init__(self, owner: SyncSessionHolder) -> None: + super().__init__(owner) + self.beekeeper = SyncBeekeeperApi(owner=self._owner) + self.beekeeper_api = self.beekeeper diff --git a/beekeepy/beekeepy/_remote_handle/api/async_api.py b/beekeepy/beekeepy/_apis/beekeeper_api/async_api.py similarity index 90% rename from beekeepy/beekeepy/_remote_handle/api/async_api.py rename to beekeepy/beekeepy/_apis/beekeeper_api/async_api.py index 336e20c1..ba109ee1 100644 --- a/beekeepy/beekeepy/_remote_handle/api/async_api.py +++ b/beekeepy/beekeepy/_apis/beekeeper_api/async_api.py @@ -1,24 +1,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -from beekeepy._remote_handle.abc.api import AbstractAsyncApi, ApiArgumentsToSerialize, AsyncHandleT -from beekeepy._remote_handle.api.apply_session_token import async_apply_session_token -from beekeepy._remote_handle.api.beekeeper_api_commons import BeekeeperApiCommons -from beekeepy._remote_handle.api.session_holder import AsyncSessionHolder +from beekeepy._apis.abc import AbstractAsyncApi, ApiArgumentsToSerialize, AsyncSendable, AsyncSessionHolder +from beekeepy._apis.apply_session_token import async_apply_session_token +from beekeepy._apis.beekeeper_api.beekeeper_api_commons import BeekeeperApiCommons from schemas.apis import beekeeper_api # noqa: TCH001 -if TYPE_CHECKING: - from beekeepy._remote_handle.beekeeper import AsyncBeekeeper, _AsyncSessionBatchHandle - -class BeekeeperApi(AbstractAsyncApi, BeekeeperApiCommons[AsyncHandleT]): +class BeekeeperApi(AbstractAsyncApi, BeekeeperApiCommons[AsyncSendable]): """Set of endpoints, that allows asynchronous communication with beekeeper service.""" api = AbstractAsyncApi._endpoint - _owner: AsyncBeekeeper | _AsyncSessionBatchHandle + _owner: AsyncSessionHolder - def __init__(self, owner: AsyncBeekeeper | _AsyncSessionBatchHandle) -> None: + def __init__(self, owner: AsyncSessionHolder) -> None: self._verify_is_owner_can_hold_session_token(owner=owner) super().__init__(owner=owner) @@ -216,14 +210,13 @@ class BeekeeperApi(AbstractAsyncApi, BeekeeperApiCommons[AsyncHandleT]): raise NotImplementedError @api - async def create_session(self, *, notifications_endpoint: str = "", salt: str = "") -> beekeeper_api.CreateSession: + async def create_session(self, *, salt: str = "") -> beekeeper_api.CreateSession: """Creates session. Note: This is called automatically when connection with beekeeper is establish, no need to call it explicitly. Args: - notifications_endpoint: endpoint on which notifications of status will be broadcasted. (defaults: "") salt: used for generation of session token Returns: diff --git a/beekeepy/beekeepy/_remote_handle/api/beekeeper_api_commons.py b/beekeepy/beekeepy/_apis/beekeeper_api/beekeeper_api_commons.py similarity index 86% rename from beekeepy/beekeepy/_remote_handle/api/beekeeper_api_commons.py rename to beekeepy/beekeepy/_apis/beekeeper_api/beekeeper_api_commons.py index 16b55fb4..718cc059 100644 --- a/beekeepy/beekeepy/_remote_handle/api/beekeeper_api_commons.py +++ b/beekeepy/beekeepy/_apis/beekeeper_api/beekeeper_api_commons.py @@ -3,10 +3,10 @@ from __future__ import annotations from abc import abstractmethod from typing import TYPE_CHECKING, Any, Generic, Protocol -from beekeepy._remote_handle.abc.api import HandleT +from beekeepy._apis.abc.api import HandleT if TYPE_CHECKING: - from beekeepy._remote_handle.api.session_holder import AsyncSessionHolder, SyncSessionHolder + from beekeepy._apis.abc import AsyncSessionHolder, SyncSessionHolder class CreateSessionActionProtocol(Protocol): diff --git a/beekeepy/beekeepy/_remote_handle/api/sync_api.py b/beekeepy/beekeepy/_apis/beekeeper_api/sync_api.py similarity index 81% rename from beekeepy/beekeepy/_remote_handle/api/sync_api.py rename to beekeepy/beekeepy/_apis/beekeeper_api/sync_api.py index 1360bf52..9b236f63 100644 --- a/beekeepy/beekeepy/_remote_handle/api/sync_api.py +++ b/beekeepy/beekeepy/_apis/beekeeper_api/sync_api.py @@ -1,23 +1,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast - -from beekeepy._remote_handle.abc.api import AbstractSyncApi, ApiArgumentsToSerialize, SyncHandleT -from beekeepy._remote_handle.api.apply_session_token import sync_apply_session_token -from beekeepy._remote_handle.api.beekeeper_api_commons import BeekeeperApiCommons -from beekeepy._remote_handle.api.session_holder import SyncSessionHolder +from beekeepy._apis.abc import AbstractSyncApi, ApiArgumentsToSerialize, SyncSendable, SyncSessionHolder +from beekeepy._apis.apply_session_token import sync_apply_session_token +from beekeepy._apis.beekeeper_api.beekeeper_api_commons import BeekeeperApiCommons from schemas.apis import beekeeper_api # noqa: TCH001 -if TYPE_CHECKING: - from beekeepy._remote_handle.beekeeper import Beekeeper, _SyncSessionBatchHandle - -class BeekeeperApi(AbstractSyncApi, BeekeeperApiCommons[SyncHandleT]): +class BeekeeperApi(AbstractSyncApi, BeekeeperApiCommons[SyncSendable]): api = AbstractSyncApi._endpoint - _owner: Beekeeper | _SyncSessionBatchHandle + _owner: SyncSessionHolder - def __init__(self, owner: Beekeeper | _SyncSessionBatchHandle) -> None: + def __init__(self, owner: SyncSessionHolder) -> None: self._verify_is_owner_can_hold_session_token(owner=owner) super().__init__(owner=owner) @@ -26,7 +20,7 @@ class BeekeeperApi(AbstractSyncApi, BeekeeperApiCommons[SyncHandleT]): ) -> ApiArgumentsToSerialize: if not self._token_required(endpoint_name): return super()._additional_arguments_actions(endpoint_name, arguments) - return sync_apply_session_token(cast(SyncSessionHolder, self._owner), arguments) + return sync_apply_session_token(self._owner, arguments) def _get_requires_session_holder_type(self) -> type[SyncSessionHolder]: return SyncSessionHolder @@ -94,7 +88,7 @@ class BeekeeperApi(AbstractSyncApi, BeekeeperApiCommons[SyncHandleT]): raise NotImplementedError @api - def create_session(self, *, notifications_endpoint: str = "", salt: str = "") -> beekeeper_api.CreateSession: + def create_session(self, *, salt: str = "") -> beekeeper_api.CreateSession: raise NotImplementedError @api diff --git a/beekeepy/beekeepy/_communication/__init__.py b/beekeepy/beekeepy/_communication/__init__.py index e69de29b..f56725c6 100644 --- a/beekeepy/beekeepy/_communication/__init__.py +++ b/beekeepy/beekeepy/_communication/__init__.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from beekeepy._communication import rules +from beekeepy._communication.abc.communicator import AbstractCommunicator +from beekeepy._communication.abc.overseer import AbstractOverseer +from beekeepy._communication.aiohttp_communicator import AioHttpCommunicator +from beekeepy._communication.httpx_communicator import HttpxCommunicator +from beekeepy._communication.overseers import CommonOverseer, StrictOverseer +from beekeepy._communication.request_communicator import RequestCommunicator +from beekeepy._communication.settings import CommunicationSettings +from beekeepy._communication.url import AnyUrl, HttpUrl, P2PUrl, Url, WsUrl + +__all__ = [ + "CommunicationSettings", + "CommonOverseer", + "StrictOverseer", + "AioHttpCommunicator", + "HttpxCommunicator", + "RequestCommunicator", + "AnyUrl", + "HttpUrl", + "P2PUrl", + "Url", + "WsUrl", + "AbstractCommunicator", + "AbstractOverseer", + "rules", +] diff --git a/beekeepy/beekeepy/_communication/abc/communicator.py b/beekeepy/beekeepy/_communication/abc/communicator.py index 2dae1791..d72446d9 100644 --- a/beekeepy/beekeepy/_communication/abc/communicator.py +++ b/beekeepy/beekeepy/_communication/abc/communicator.py @@ -7,13 +7,13 @@ from threading import Thread from typing import TYPE_CHECKING, Any, Awaitable from beekeepy._communication.settings import CommunicationSettings -from beekeepy._interface.settings_holder import SharedSettingsHolder -from beekeepy._interface.stopwatch import Stopwatch +from beekeepy._utilities.settings_holder import SharedSettingsHolder +from beekeepy._utilities.stopwatch import Stopwatch from beekeepy.exceptions import CommunicationError, TimeoutExceededError, UnknownDecisionPathError if TYPE_CHECKING: - from beekeepy._interface.stopwatch import StopwatchResult - from beekeepy._interface.url import HttpUrl + from beekeepy._communication.url import HttpUrl + from beekeepy._utilities.stopwatch import StopwatchResult class AbstractCommunicator(SharedSettingsHolder[CommunicationSettings], ABC): diff --git a/beekeepy/beekeepy/_communication/abc/http_server_observer.py b/beekeepy/beekeepy/_communication/abc/http_server_observer.py deleted file mode 100644 index 6d4efcee..00000000 --- a/beekeepy/beekeepy/_communication/abc/http_server_observer.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any - - -class HttpServerObserver(ABC): - @abstractmethod - async def data_received(self, data: dict[str, Any]) -> None: - """Called when any data is received via PUT method. - - Args: - data: data received as body - - Returns: - Nothing. - """ diff --git a/beekeepy/beekeepy/_communication/abc/notification_handler.py b/beekeepy/beekeepy/_communication/abc/notification_handler.py deleted file mode 100644 index da69e8b5..00000000 --- a/beekeepy/beekeepy/_communication/abc/notification_handler.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any - -from beekeepy._communication.abc.http_server_observer import HttpServerObserver -from schemas.notifications import KnownNotificationT, Notification - - -class NotificationHandler(HttpServerObserver, ABC): - async def data_received(self, data: dict[str, Any]) -> None: - deserialized_notification = Notification(**data) - await self.handle_notification(deserialized_notification) - - @abstractmethod - async def handle_notification(self, notification: Notification[KnownNotificationT]) -> None: - """Method called after properly serializing notification. - - Args: - notification: received notification object - """ diff --git a/beekeepy/beekeepy/_communication/abc/overseer.py b/beekeepy/beekeepy/_communication/abc/overseer.py index 30d75296..756eae86 100644 --- a/beekeepy/beekeepy/_communication/abc/overseer.py +++ b/beekeepy/beekeepy/_communication/abc/overseer.py @@ -5,19 +5,16 @@ from abc import ABC, abstractmethod from enum import IntEnum from typing import TYPE_CHECKING, Any, Callable, ClassVar, Sequence -from beekeepy._interface.context import SelfContextSync -from beekeepy.exceptions import ( - GroupedErrorsError, - UnknownDecisionPathError, -) +from beekeepy._utilities.context import SelfContextSync +from beekeepy.exceptions import GroupedErrorsError, Json, UnknownDecisionPathError if TYPE_CHECKING: from types import TracebackType from beekeepy._communication.abc.communicator import AbstractCommunicator from beekeepy._communication.abc.rules import Rules, RulesClassifier - from beekeepy._interface.url import HttpUrl - from beekeepy.exceptions import Json, OverseerError + from beekeepy._communication.url import HttpUrl + from beekeepy.exceptions import OverseerError __all__ = ["AbstractOverseer"] diff --git a/beekeepy/beekeepy/_communication/abc/rules.py b/beekeepy/beekeepy/_communication/abc/rules.py index 335f83ae..3e6114d2 100644 --- a/beekeepy/beekeepy/_communication/abc/rules.py +++ b/beekeepy/beekeepy/_communication/abc/rules.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Sequence if TYPE_CHECKING: - from beekeepy._interface.url import HttpUrl + from beekeepy._communication.url import HttpUrl from beekeepy.exceptions import Json, OverseerError diff --git a/beekeepy/beekeepy/_communication/aiohttp_communicator.py b/beekeepy/beekeepy/_communication/aiohttp_communicator.py index f20e9442..07095537 100644 --- a/beekeepy/beekeepy/_communication/aiohttp_communicator.py +++ b/beekeepy/beekeepy/_communication/aiohttp_communicator.py @@ -13,8 +13,8 @@ from beekeepy.exceptions import CommunicationError, UnknownDecisionPathError if TYPE_CHECKING: from beekeepy._communication.settings import CommunicationSettings - from beekeepy._interface.stopwatch import StopwatchResult - from beekeepy._interface.url import HttpUrl + from beekeepy._communication.url import HttpUrl + from beekeepy._utilities.stopwatch import StopwatchResult class AioHttpCommunicator(AbstractCommunicator): diff --git a/beekeepy/beekeepy/_communication/appbase_notification_handler.py b/beekeepy/beekeepy/_communication/appbase_notification_handler.py deleted file mode 100644 index c9d0241a..00000000 --- a/beekeepy/beekeepy/_communication/appbase_notification_handler.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from beekeepy._communication.notification_decorator import notification -from beekeepy._communication.universal_notification_server import UniversalNotificationHandler -from schemas.notifications import Error, Status, WebserverListening - -if TYPE_CHECKING: - from schemas.notifications import Notification - - -class AppbaseNotificationHandler(UniversalNotificationHandler): - @notification(WebserverListening, condition=lambda n: n.value.type_ == "HTTP") - async def _on_http_webserver_bind(self, notification: Notification[WebserverListening]) -> None: - await self.on_http_webserver_bind(notification) - - @notification(WebserverListening, condition=lambda n: n.value.type_ == "WS") - async def _on_ws_webserver_bind(self, notification: Notification[WebserverListening]) -> None: - await self.on_ws_webserver_bind(notification) - - @notification(Status) - async def _on_status_changed(self, notification: Notification[Status]) -> None: - await self.on_status_changed(notification) - - @notification(Error) - async def _on_error(self, notification: Notification[Error]) -> None: - await self.on_error(notification) - - async def on_http_webserver_bind(self, notification: Notification[WebserverListening]) -> None: - """Called when hived reports http server to be ready.""" - - async def on_ws_webserver_bind(self, notification: Notification[WebserverListening]) -> None: - """Called when hived reports ws server to be ready.""" - - async def on_status_changed(self, notification: Notification[Status]) -> None: - """Called when status of notifier changed.""" - - async def on_error(self, notification: Notification[Error]) -> None: - """Called when notifier reports an error.""" diff --git a/beekeepy/beekeepy/_communication/async_server.py b/beekeepy/beekeepy/_communication/async_server.py deleted file mode 100644 index 5e6de354..00000000 --- a/beekeepy/beekeepy/_communication/async_server.py +++ /dev/null @@ -1,104 +0,0 @@ -from __future__ import annotations - -import asyncio -from http import HTTPStatus -from typing import TYPE_CHECKING - -from aiohttp import web -from typing_extensions import Self - -from beekeepy._interface.context import ContextAsync -from beekeepy._interface.url import HttpUrl -from beekeepy.exceptions import BeekeepyError - -if TYPE_CHECKING: - from socket import socket - - from beekeepy._communication.abc.http_server_observer import HttpServerObserver - - -class AsyncHttpServerError(BeekeepyError): - pass - - -class ServerNotRunningError(AsyncHttpServerError): - def __init__(self) -> None: - super().__init__("Server is not running. Call run() first.") - - -class ServerAlreadyRunningError(AsyncHttpServerError): - def __init__(self) -> None: - super().__init__("Server is already running. Call close() first.") - - -class ServerSetupError(AsyncHttpServerError): - def __init__(self, message: str) -> None: - super().__init__(message) - - -class AsyncHttpServer(ContextAsync[Self]): # type: ignore[misc] - __ADDRESS = HttpUrl("0.0.0.0:0") - - def __init__(self, observer: HttpServerObserver, notification_endpoint: HttpUrl | None) -> None: - self.__observer = observer - self._app = web.Application() - self.__site: web.TCPSite | None = None - self.__running: bool = False - self.__notification_endpoint = notification_endpoint - self._setup_routes() - - def _setup_routes(self) -> None: - async def handle_put_method(request: web.Request) -> web.Response: - await self.__observer.data_received(await request.json()) - return web.Response(status=HTTPStatus.NO_CONTENT) - - self._app.router.add_route("PUT", "/", handle_put_method) - - @property - def port(self) -> int: - if not self.__site: - raise ServerNotRunningError - server: asyncio.base_events.Server | None = self.__site._server # type: ignore[assignment] - if server is None: - raise ServerSetupError("self.__site.server is None") - - server_socket: socket = server.sockets[0] - address_tuple: tuple[str, int] = server_socket.getsockname() - - if not ( - isinstance(address_tuple, tuple) and isinstance(address_tuple[0], str) and isinstance(address_tuple[1], int) - ): - raise ServerSetupError(f"address_tuple has not recognizable types: {address_tuple}") - - return address_tuple[1] - - async def run(self) -> None: - if self.__site: - raise ServerAlreadyRunningError - - time_between_checks_is_server_running = 0.5 - - runner = web.AppRunner(self._app, access_log=False) - await runner.setup() - address = self.__notification_endpoint or self.__ADDRESS - self.__site = web.TCPSite(runner, address.address, address.port) - await self.__site.start() - self.__running = True - try: - while self.__running: - await asyncio.sleep(time_between_checks_is_server_running) - finally: - await self.__site.stop() - self.__site = None - - def close(self) -> None: - if not self.__site: - raise ServerNotRunningError - self.__running = False - - async def _aenter(self) -> Self: - await self.run() - return self - - async def _afinally(self) -> None: - self.close() diff --git a/beekeepy/beekeepy/_communication/httpx_communicator.py b/beekeepy/beekeepy/_communication/httpx_communicator.py index 30aaf85b..2162cc4b 100644 --- a/beekeepy/beekeepy/_communication/httpx_communicator.py +++ b/beekeepy/beekeepy/_communication/httpx_communicator.py @@ -11,8 +11,8 @@ from beekeepy.exceptions import CommunicationError if TYPE_CHECKING: from beekeepy._communication.settings import CommunicationSettings - from beekeepy._interface.stopwatch import StopwatchResult - from beekeepy._interface.url import HttpUrl + from beekeepy._communication.url import HttpUrl + from beekeepy._utilities.stopwatch import StopwatchResult ClientTypes = httpx.AsyncClient | httpx.Client diff --git a/beekeepy/beekeepy/_communication/notification_decorator.py b/beekeepy/beekeepy/_communication/notification_decorator.py deleted file mode 100644 index caa22a44..00000000 --- a/beekeepy/beekeepy/_communication/notification_decorator.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -from collections.abc import Awaitable, Callable # noqa: TCH003 -from typing import Any, Generic - -from pydantic.generics import GenericModel - -from schemas.notifications import KnownNotificationT, Notification - - -class _NotificationHandlerWrapper(GenericModel, Generic[KnownNotificationT]): - notification_name: str - notification_handler: Callable[[Any, Notification[KnownNotificationT]], Awaitable[None]] - notification_condition: Callable[[Notification[KnownNotificationT]], bool] - - async def call(self, this: Any, notification: Notification[KnownNotificationT]) -> None: - await self.notification_handler(this, notification) - - async def __call__(self, this: Any, notification: Notification[KnownNotificationT]) -> Any: - return self.call(this, notification) - - -def notification( - type_: type[KnownNotificationT], - /, - *, - condition: Callable[[Notification[KnownNotificationT]], bool] | None = None, -) -> Callable[ - [Callable[[Any, Notification[KnownNotificationT]], Awaitable[None]]], - _NotificationHandlerWrapper[KnownNotificationT], -]: - def wrapper( - callback: Callable[[Any, Notification[KnownNotificationT]], Awaitable[None]], - ) -> _NotificationHandlerWrapper[KnownNotificationT]: - result_cls = _NotificationHandlerWrapper[type_] # type: ignore[valid-type] - result_cls.update_forward_refs(**locals()) - return result_cls( # type: ignore[return-value] - notification_name=type_.get_notification_name(), - notification_handler=callback, - notification_condition=condition or (lambda _: True), - ) - - return wrapper diff --git a/beekeepy/beekeepy/_communication/request_communicator.py b/beekeepy/beekeepy/_communication/request_communicator.py index 258b494b..71b73d06 100644 --- a/beekeepy/beekeepy/_communication/request_communicator.py +++ b/beekeepy/beekeepy/_communication/request_communicator.py @@ -11,8 +11,8 @@ from beekeepy.exceptions import CommunicationError if TYPE_CHECKING: from beekeepy._communication.settings import CommunicationSettings - from beekeepy._interface.stopwatch import StopwatchResult - from beekeepy._interface.url import HttpUrl + from beekeepy._communication.url import HttpUrl + from beekeepy._utilities.stopwatch import StopwatchResult class RequestCommunicator(AbstractCommunicator): diff --git a/beekeepy/beekeepy/_communication/settings.py b/beekeepy/beekeepy/_communication/settings.py index ee6feaef..3a0d3a7c 100644 --- a/beekeepy/beekeepy/_communication/settings.py +++ b/beekeepy/beekeepy/_communication/settings.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, cast from pydantic import BaseModel, Field -from beekeepy._interface.url import Url +from beekeepy._communication.url import Url if TYPE_CHECKING: from collections.abc import Callable diff --git a/beekeepy/beekeepy/_communication/universal_notification_server.py b/beekeepy/beekeepy/_communication/universal_notification_server.py deleted file mode 100644 index 7813e49b..00000000 --- a/beekeepy/beekeepy/_communication/universal_notification_server.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import asyncio -from collections import defaultdict -from threading import Thread -from time import sleep -from typing import TYPE_CHECKING, Any, Final - -from beekeepy._communication.abc.notification_handler import NotificationHandler -from beekeepy._communication.async_server import AsyncHttpServer -from beekeepy._communication.notification_decorator import _NotificationHandlerWrapper -from beekeepy._interface.context import ContextSync -from beekeepy.exceptions import BeekeepyError - -if TYPE_CHECKING: - from beekeepy._interface.url import HttpUrl - from schemas.notifications import KnownNotificationT, Notification - - -class UnhandledNotificationError(BeekeepyError): - def __init__(self, notification: Notification[KnownNotificationT]) -> None: - super().__init__( - f"Notification `{notification.name}` does not have any registered method to be passed to.", notification - ) - - -class UniversalNotificationHandler(NotificationHandler): - def __init__(self, *args: Any, **kwargs: Any) -> None: - self.__registered_notifications: defaultdict[str, list[_NotificationHandlerWrapper[Any]]] = defaultdict(list) - super().__init__(*args, **kwargs) - - def setup(self) -> None: - for member_name in dir(self): - member_value = getattr(self, member_name) - if isinstance(member_value, _NotificationHandlerWrapper): - self.__registered_notifications[member_value.notification_name].append(member_value) - - async def handle_notification(self, notification: Notification[KnownNotificationT]) -> None: - if (callbacks := self.__registered_notifications.get(notification.name)) is not None: - for callback in callbacks: - if callback.notification_condition(notification): - await callback.call(self, notification) - return - - raise UnhandledNotificationError(notification) - - -class UniversalNotificationServer(ContextSync[int]): - def __init__( - self, - event_handler: UniversalNotificationHandler, - notification_endpoint: HttpUrl | None = None, - *, - thread_name: str | None = None, - ) -> None: - self.__event_handler = event_handler - self.__event_handler.setup() - self.__http_server = AsyncHttpServer(self.__event_handler, notification_endpoint=notification_endpoint) - self.__server_thread: Thread | None = None - self.__thread_name = thread_name - - def run(self) -> int: - time_to_launch_notification_server: Final[float] = 0.5 - assert self.__server_thread is None, "Server thread is not None; Is server still running?" - - self.__server_thread = Thread(target=asyncio.run, args=(self.__http_server.run(),), name=self.__thread_name) - self.__server_thread.start() - sleep(time_to_launch_notification_server) - return self.__http_server.port - - def close(self) -> None: - if self.__server_thread is None: - return - - self.__http_server.close() - self.__server_thread.join() - self.__server_thread = None - - @property - def port(self) -> int: - return self.__http_server.port - - def _enter(self) -> int: - return self.run() - - def _finally(self) -> None: - self.close() diff --git a/beekeepy/beekeepy/_interface/url.py b/beekeepy/beekeepy/_communication/url.py similarity index 88% rename from beekeepy/beekeepy/_interface/url.py rename to beekeepy/beekeepy/_communication/url.py index f7a8937f..c7ce7d39 100644 --- a/beekeepy/beekeepy/_interface/url.py +++ b/beekeepy/beekeepy/_communication/url.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Generic, Literal, TypeVar, get_args from urllib.parse import urlparse +from typing_extensions import Self + P2PProtocolT = Literal[""] HttpProtocolT = Literal["http", "https"] WsProtocolT = Literal["ws", "wss"] @@ -18,6 +20,7 @@ class Url(Generic[ProtocolT]): if protocol is not None and protocol not in allowed_proto: raise ValueError(f"Unknown protocol: `{protocol}`, allowed: {allowed_proto}") + target_protocol: str = protocol or self._default_protocol() if isinstance(url, Url): self.__protocol: str = url.__protocol self.__address: str = url.__address @@ -78,6 +81,14 @@ class Url(Generic[ProtocolT]): """ return [""] + @classmethod + def factory(cls, *, port: int = 0, address: str = "127.0.0.1") -> Self: + return cls((f"{cls._default_protocol()}://" if cls._default_protocol() else "") + f"{address}:{port}") + + @classmethod + def _default_protocol(cls) -> str: + return cls._allowed_protocols()[0] + class HttpUrl(Url[HttpProtocolT]): @classmethod diff --git a/beekeepy/beekeepy/_executable/__init__.py b/beekeepy/beekeepy/_executable/__init__.py index a734003e..6b444e5d 100644 --- a/beekeepy/beekeepy/_executable/__init__.py +++ b/beekeepy/beekeepy/_executable/__init__.py @@ -1,13 +1,29 @@ from __future__ import annotations -from beekeepy._executable.arguments.arguments import Arguments -from beekeepy._executable.arguments.beekeeper_arguments import BeekeeperArguments +from beekeepy._executable.abc import ( + Arguments, + ArgumentT, + Config, + ConfigT, + Executable, + StreamRepresentation, + StreamsHolder, +) +from beekeepy._executable.beekeeper_arguments import BeekeeperArguments +from beekeepy._executable.beekeeper_config import BeekeeperConfig from beekeepy._executable.beekeeper_executable import BeekeeperExecutable -from beekeepy._executable.executable import Executable +from beekeepy._utilities.key_pair import KeyPair __all__ = [ - "Arguments", + "BeekeeperConfig", "BeekeeperArguments", "BeekeeperExecutable", + "Arguments", "Executable", + "Config", + "StreamRepresentation", + "StreamsHolder", + "ArgumentT", + "ConfigT", + "KeyPair", ] diff --git a/beekeepy/beekeepy/_executable/abc/__init__.py b/beekeepy/beekeepy/_executable/abc/__init__.py new file mode 100644 index 00000000..57c9222f --- /dev/null +++ b/beekeepy/beekeepy/_executable/abc/__init__.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from beekeepy._executable.abc.arguments import Arguments +from beekeepy._executable.abc.config import Config +from beekeepy._executable.abc.executable import ArgumentT, ConfigT, Executable +from beekeepy._executable.abc.streams import StreamRepresentation, StreamsHolder + +__all__ = [ + "Arguments", + "Executable", + "StreamsHolder", + "StreamRepresentation", + "Config", + "ArgumentT", + "ConfigT", +] diff --git a/beekeepy/beekeepy/_executable/abc/arguments.py b/beekeepy/beekeepy/_executable/abc/arguments.py new file mode 100644 index 00000000..2fe6085a --- /dev/null +++ b/beekeepy/beekeepy/_executable/abc/arguments.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from pydantic import Field + +from schemas._preconfigured_base_model import PreconfiguredBaseModel + +if TYPE_CHECKING: + from typing_extensions import Self + + +__all__ = ["Arguments"] + + +class CliParser: + @classmethod + def parse_cli_input(cls, cli: list[str]) -> dict[str, str | list[str] | bool]: + ordered_cli: dict[str, str | set[str] | None] = {} + previous_key: str | None = None + for item in cli: + key, value = cls._preprocess_option(item) + if key.startswith("__"): + previous_key = key[2:] + ordered_cli[previous_key] = value + continue + if key.startswith("_"): + previous_key = key[1:] + ordered_cli[previous_key] = value + continue + + if key in ordered_cli: + if isinstance((dict_value := ordered_cli[key]), set): + dict_value.add(item) + elif ordered_cli[key] is None: + assert isinstance(item, str), "parsing failed, item is not string" # mypy check + ordered_cli[key] = item + else: # if ordered_cli[key] is not None and is not set + assert isinstance(item, str), "parsing failed, item is not string" # mypy check + dict_value = ordered_cli[key] + assert isinstance(dict_value, str), "parsing failed, dict_value is not string" # mypy check + ordered_cli[key] = {dict_value, item} + continue + + assert ( + previous_key is not None + ), "parsing failed, previous_key was not set and following argument is not prefixed" + ordered_cli[previous_key] = item + previous_key = None + + return cls._convert_sets_to_lists_and_none_to_boolean(ordered_cli) + + @classmethod + def _convert_sets_to_lists_and_none_to_boolean( + cls, ordered_cli: dict[str, str | set[str] | None] + ) -> dict[str, str | list[str] | bool]: + result: dict[str, str | list[str] | bool] = {} + for key, value in ordered_cli.items(): + if isinstance(value, set): + result[key] = list(value) + elif value is None: + result[key] = True + else: + result[key] = value + return result + + @classmethod + def _preprocess_option(cls, item: str) -> tuple[str, str | None]: + key = item + value = None + if "=" in item: + key, value = item.split("=") + + key = key.replace("-", "_") + return key, value + + +class Arguments(PreconfiguredBaseModel, ABC): + help_: bool = Field(alias="help", default=False) + version: bool = False + dump_config: bool = False + + class Config: + arbitrary_types_allowed = True + + def __convert_member_name_to_cli_value(self, member_name: str) -> str: + return member_name.replace("_", "-") + + def __convert_member_value_to_string(self, member_value: int | str | Path | Any) -> str: + if isinstance(member_value, bool): + return "" + if isinstance(member_value, str): + return member_value + if isinstance(member_value, int): + return str(member_value) + if isinstance(member_value, Path): + return member_value.as_posix() + if isinstance(result := self._convert_member_value_to_string_default(member_value=member_value), str): + return result + raise TypeError("Invalid type") + + @abstractmethod + def _convert_member_value_to_string_default(self, member_value: Any) -> str | Any: ... + + def __prepare_arguments(self, pattern: str) -> list[str]: + data = self.dict(by_alias=True, exclude_none=True, exclude_unset=True, exclude_defaults=True) + cli_arguments: list[str] = [] + for k, v in data.items(): + cli_arguments.append(pattern.format(self.__convert_member_name_to_cli_value(k))) + cli_arguments.append(self.__convert_member_value_to_string(v)) + return cli_arguments + + def process(self, *, with_prefix: bool = True) -> list[str]: + pattern = self._generate_argument_prefix(with_prefix=with_prefix) + return self.__prepare_arguments(pattern) + + def _generate_argument_prefix(self, *, with_prefix: bool) -> str: + return "--{0}" if with_prefix else "{0}" + + def update_with(self, other: Self | None) -> None: + if other is None: + return + + for other_name, other_value in other.dict(exclude_unset=True, exclude_defaults=True, exclude_none=True).items(): + assert isinstance(other_name, str), "Member name has to be string" + setattr(self, other_name, other_value) + + @classmethod + def parse_cli_input(cls, cli: list[str]) -> Self: + return cls(**CliParser.parse_cli_input(cli)) + + @classmethod + def just_get_help(cls) -> Self: + return cls(help_=True) + + @classmethod + def just_get_version(cls) -> Self: + return cls(version=True) + + @classmethod + def just_dump_config(cls) -> Self: + return cls(dump_config=True) diff --git a/beekeepy/beekeepy/_interface/config.py b/beekeepy/beekeepy/_executable/abc/config.py similarity index 59% rename from beekeepy/beekeepy/_interface/config.py rename to beekeepy/beekeepy/_executable/abc/config.py index 4ae8f183..54d1bf42 100644 --- a/beekeepy/beekeepy/_interface/config.py +++ b/beekeepy/beekeepy/_executable/abc/config.py @@ -4,9 +4,10 @@ from pathlib import Path from types import UnionType from typing import TYPE_CHECKING, Any, ClassVar, get_args +from loguru import logger from pydantic import BaseModel -from beekeepy._interface.url import Url +from beekeepy._communication import Url from beekeepy.exceptions import InvalidOptionError if TYPE_CHECKING: @@ -25,26 +26,30 @@ class Config(BaseModel): out_file.write("# config automatically generated by helpy\n") for member_name, member_value in self.__dict__.items(): if member_value is not None: - out_file.write( - f"{self._convert_member_name_to_config_name(member_name)}={self._convert_member_value_to_config_value(member_value)}\n" - ) + if isinstance(member_value, list) and len(member_value) == 0: + continue + + entry_name = self._convert_member_name_to_config_name(member_name) + entry_value = self._convert_member_value_to_config_value(member_name, member_value) + for value in [entry_value] if not isinstance(entry_value, list) else entry_value: + out_file.write(f"{entry_name} = {value}\n") @classmethod def load(cls, source: Path) -> Self: source = source / Config.DEFAULT_FILE_NAME if source.is_dir() else source assert source.exists(), "Given file does not exists." fields = cls.__fields__ - values_to_write = {} + values_to_write: dict[str, Any] = {} with source.open("rt", encoding="utf-8") as in_file: for line in in_file: if (line := line.strip("\n")) and not line.startswith("#"): config_name, config_value = line.split("=") member_name = cls._convert_config_name_to_member_name(config_name) member_type = fields[member_name].annotation - if isinstance(member_type, UnionType) and get_args(member_type)[-1] == type(None): + if isinstance(member_type, UnionType) and (type(None) in get_args(member_type)): member_type = get_args(member_type)[0] values_to_write[member_name] = cls._convert_config_value_to_member_value( - config_value, expected=member_type + config_value, expected=member_type, current_value=values_to_write.get(member_name) ) return cls(**values_to_write) @@ -57,9 +62,9 @@ class Config(BaseModel): return config_name.strip().replace("-", "_") @classmethod - def _convert_member_value_to_config_value(cls, member_value: Any) -> str: + def _convert_member_value_to_config_value(cls, member_name: str, member_value: Any) -> str | list[str]: # noqa: ARG003 if isinstance(member_value, list): - return " ".join(member_value) + return member_value if isinstance(member_value, bool): return "yes" if member_value else "no" @@ -73,8 +78,8 @@ class Config(BaseModel): return str(member_value) @classmethod - def _convert_config_value_to_member_value( # noqa: PLR0911 - cls, config_value: str, *, expected: type[Any] + def _convert_config_value_to_member_value( # noqa: PLR0911, C901 + cls, config_value: str, *, expected: type[Any], current_value: Any | None ) -> Any | None: config_value = config_value.strip() if config_value is None: @@ -83,8 +88,21 @@ class Config(BaseModel): if expected == Path: return Path(config_value.replace('"', "")) - if expected == list[str]: - return config_value.split() + if issubclass(expected, list) or "list" in str(expected): + list_arg_t = get_args(expected)[0] + if len(get_args(list_arg_t)): # in case of unions + list_arg_t = get_args(list_arg_t)[0] + logger.info(f"{list_arg_t=}") + values = [ + cls._convert_config_value_to_member_value(value, expected=list_arg_t, current_value=None) + for value in config_value.split() + ] + if current_value is not None: + if isinstance(current_value, list): + current_value.extend(values) + return current_value + return [*values, current_value] + return values if expected == Url: return Url(config_value) @@ -99,4 +117,10 @@ class Config(BaseModel): raise InvalidOptionError(f"Expected `yes` or `no`, got: `{config_value}`") + if "str" in str(expected): + return config_value.strip('"') + + if isinstance(expected, type) and issubclass(expected, int | str) and hasattr(expected, "validate"): + return expected.validate(config_value) + return expected(config_value) if expected is not None else None diff --git a/beekeepy/beekeepy/_executable/executable.py b/beekeepy/beekeepy/_executable/abc/executable.py similarity index 62% rename from beekeepy/beekeepy/_executable/executable.py rename to beekeepy/beekeepy/_executable/abc/executable.py index 4f171554..476e3d10 100644 --- a/beekeepy/beekeepy/_executable/executable.py +++ b/beekeepy/beekeepy/_executable/abc/executable.py @@ -3,17 +3,21 @@ from __future__ import annotations import os import signal import subprocess -import warnings +import time from abc import ABC, abstractmethod +from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Generic, TypeVar -from beekeepy._executable.arguments.arguments import Arguments -from beekeepy._executable.streams import StreamsHolder -from beekeepy._interface.config import Config -from beekeepy._interface.context import ContextSync -from beekeepy.exceptions import BeekeeperIsNotRunningError, TimeoutReachWhileCloseError +import psutil + +from beekeepy._executable.abc.arguments import Arguments +from beekeepy._executable.abc.config import Config +from beekeepy._executable.abc.streams import StreamsHolder +from beekeepy._utilities.context import ContextSync +from beekeepy.exceptions import ExecutableIsNotRunningError, TimeoutReachWhileCloseError if TYPE_CHECKING: + from collections.abc import Iterator from pathlib import Path from loguru import Logger @@ -21,7 +25,7 @@ if TYPE_CHECKING: class Closeable(ABC): @abstractmethod - def close(self) -> None: ... + def close(self, timeout_secs: float = 10.0) -> None: ... class AutoCloser(ContextSync[None]): @@ -70,15 +74,41 @@ class Executable(Closeable, Generic[ConfigT, ArgumentT]): def config(self) -> ConfigT: return self.__config - def run( + @property + def arguments(self) -> ArgumentT: + return self.__arguments + + @property + def executable_path(self) -> Path: + return self.__executable_path + + def _run( self, *, blocking: bool, - arguments: ArgumentT | None = None, environ: dict[str, str] | None = None, propagate_sigint: bool = True, + save_config: bool = True, ) -> AutoCloser: - command, environment_variables = self.__prepare(arguments=arguments, environ=environ) + return self.__run( + blocking=blocking, + arguments=self.arguments, + environ=environ, + propagate_sigint=propagate_sigint, + save_config=save_config, + ) + + def __run( # noqa: PLR0913 + self, + *, + blocking: bool, + arguments: ArgumentT, + environ: dict[str, str] | None = None, + propagate_sigint: bool = True, + save_config: bool = True, + ) -> AutoCloser: + command, environment_variables = self.__prepare(arguments=arguments, environ=environ, save_config=save_config) + self._logger.info(f"starting `{self.__executable_path.stem}` as: `{command}`") if blocking: with self.__files.stdout as stdout, self.__files.stderr as stderr: @@ -114,9 +144,11 @@ class Executable(Closeable, Generic[ConfigT, ArgumentT]): return result.decode().strip() def __prepare( - self, arguments: ArgumentT | None, environ: dict[str, str] | None + self, + arguments: ArgumentT, + environ: dict[str, str] | None, + save_config: bool = True, # noqa: FBT001, FBT002 ) -> tuple[list[str], dict[str, str]]: - arguments = arguments or self.__arguments environ = environ or {} self.__working_directory.mkdir(exist_ok=True) @@ -127,7 +159,8 @@ class Executable(Closeable, Generic[ConfigT, ArgumentT]): environment_variables = dict(os.environ) environment_variables.update(environ) - self.config.save(self.working_directory) + if save_config: + self.config.save(self.working_directory) return command, environment_variables @@ -136,7 +169,7 @@ class Executable(Closeable, Generic[ConfigT, ArgumentT]): def detach(self) -> int: if self.__process is None: - raise BeekeeperIsNotRunningError + raise ExecutableIsNotRunningError pid = self.pid self.__process = None self.__files.close() @@ -158,16 +191,6 @@ class Executable(Closeable, Generic[ConfigT, ArgumentT]): self.__process = None self.__files.close() - def __warn_if_pid_files_exists(self) -> None: - if self.__pid_files_exists(): - warnings.warn( - f"PID file has not been removed, malfunction may occur. Working directory: {self.working_directory}", - stacklevel=2, - ) - - def __pid_files_exists(self) -> bool: - return len(list(self.working_directory.glob("*.pid"))) > 0 - def is_running(self) -> bool: if not self.__process: return False @@ -177,6 +200,17 @@ class Executable(Closeable, Generic[ConfigT, ArgumentT]): def log_has_phrase(self, text: str) -> bool: return text in self.__files + @contextmanager + def restore_arguments(self, new_arguments: ArgumentT | None) -> Iterator[None]: + __backup = self.__arguments + self.__arguments = new_arguments or self.__arguments + try: + yield + except: # noqa: TRY302 # https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager + raise + finally: + self.__arguments = __backup + @abstractmethod def _construct_config(self) -> ConfigT: ... @@ -184,9 +218,20 @@ class Executable(Closeable, Generic[ConfigT, ArgumentT]): def _construct_arguments(self) -> ArgumentT: ... def generate_default_config(self) -> ConfigT: - path_to_config = self.working_directory / (Config.DEFAULT_FILE_NAME) - self.run(blocking=True, arguments=self.__arguments.just_dump_config()) - temp_path_to_file = path_to_config.rename(Config.DEFAULT_FILE_NAME + ".tmp") + if not self.working_directory.exists(): + self.working_directory.mkdir(parents=True) + orig_path_to_config: Path | None = None + path_to_config = self.working_directory / Config.DEFAULT_FILE_NAME + if path_to_config.exists(): + orig_path_to_config = path_to_config.rename( + path_to_config.with_suffix(".ini.orig") + ) # temporary move it to not interfere with config generation + arguments = self._construct_arguments() + arguments.dump_config = True + self.__run(blocking=True, arguments=arguments, save_config=False) + temp_path_to_file = path_to_config.rename(path_to_config.with_suffix(".ini.tmp")) + if orig_path_to_config is not None: + orig_path_to_config.rename(path_to_config) return self.config.load(temp_path_to_file) def get_help_text(self) -> str: @@ -194,3 +239,13 @@ class Executable(Closeable, Generic[ConfigT, ArgumentT]): def version(self) -> str: return self.run_and_get_output(arguments=self.__arguments.just_get_version()) + + def reserved_ports(self, *, timeout_seconds: int = 10) -> list[int]: + assert self.is_running(), "Cannot obtain reserved ports for not started executable" + start = time.perf_counter() + while start + timeout_seconds >= time.perf_counter(): + connections = psutil.net_connections("inet4") + reserved_ports = [connection.laddr[1] for connection in connections if connection.pid == self.pid] # type: ignore[misc] + if reserved_ports: + return reserved_ports + raise TimeoutError diff --git a/beekeepy/beekeepy/_executable/streams.py b/beekeepy/beekeepy/_executable/abc/streams.py similarity index 98% rename from beekeepy/beekeepy/_executable/streams.py rename to beekeepy/beekeepy/_executable/abc/streams.py index 27b5eef1..1e615284 100644 --- a/beekeepy/beekeepy/_executable/streams.py +++ b/beekeepy/beekeepy/_executable/abc/streams.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, TextIO, cast -from beekeepy._interface.context import ContextSync +from beekeepy._utilities.context import ContextSync if TYPE_CHECKING: from pathlib import Path diff --git a/beekeepy/beekeepy/_executable/arguments/arguments.py b/beekeepy/beekeepy/_executable/arguments/arguments.py deleted file mode 100644 index 4a248218..00000000 --- a/beekeepy/beekeepy/_executable/arguments/arguments.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from pathlib import Path -from typing import TYPE_CHECKING, Any - -from pydantic import Field - -from schemas._preconfigured_base_model import PreconfiguredBaseModel - -if TYPE_CHECKING: - from typing_extensions import Self - - -class Arguments(PreconfiguredBaseModel, ABC): - help_: bool = Field(alias="help", default=False) - version: bool = False - dump_config: bool = False - - class Config: - arbitrary_types_allowed = True - - def __convert_member_name_to_cli_value(self, member_name: str) -> str: - return member_name.replace("_", "-") - - def __convert_member_value_to_string(self, member_value: int | str | Path | Any) -> str: - if isinstance(member_value, bool): - return "" - if isinstance(member_value, str): - return member_value - if isinstance(member_value, int): - return str(member_value) - if isinstance(member_value, Path): - return member_value.as_posix() - if isinstance(result := self._convert_member_value_to_string_default(member_value=member_value), str): - return result - raise TypeError("Invalid type") - - @abstractmethod - def _convert_member_value_to_string_default(self, member_value: Any) -> str | Any: ... - - def __prepare_arguments(self, pattern: str) -> list[str]: - data = self.dict(by_alias=True, exclude_none=True, exclude_unset=True, exclude_defaults=True) - cli_arguments: list[str] = [] - for k, v in data.items(): - cli_arguments.append(pattern.format(self.__convert_member_name_to_cli_value(k))) - cli_arguments.append(self.__convert_member_value_to_string(v)) - return cli_arguments - - def process(self, *, with_prefix: bool = True) -> list[str]: - pattern = "--{0}" if with_prefix else "{0}" - return self.__prepare_arguments(pattern) - - @classmethod - def just_get_help(cls) -> Self: - return cls(help_=True) - - @classmethod - def just_get_version(cls) -> Self: - return cls(version=True) - - @classmethod - def just_dump_config(cls) -> Self: - return cls(dump_config=True) diff --git a/beekeepy/beekeepy/_executable/arguments/beekeeper_arguments.py b/beekeepy/beekeepy/_executable/beekeeper_arguments.py similarity index 82% rename from beekeepy/beekeepy/_executable/arguments/beekeeper_arguments.py rename to beekeepy/beekeepy/_executable/beekeeper_arguments.py index 525fde4f..5af2501b 100644 --- a/beekeepy/beekeepy/_executable/arguments/beekeeper_arguments.py +++ b/beekeepy/beekeepy/_executable/beekeeper_arguments.py @@ -4,16 +4,14 @@ from dataclasses import dataclass from pathlib import Path from typing import Any, ClassVar, Literal -from beekeepy._executable.arguments.arguments import Arguments -from beekeepy._interface.url import HttpUrl +from beekeepy._communication import HttpUrl +from beekeepy._executable.abc.arguments import Arguments class BeekeeperArgumentsDefaults: DEFAULT_BACKTRACE: ClassVar[Literal["yes", "no"]] = "yes" - DEFAULT_DATA_DIR: ClassVar[Path] = Path.cwd() DEFAULT_EXPORT_KEYS_WALLET: ClassVar[ExportKeysWalletParams | None] = None DEFAULT_LOG_JSON_RPC: ClassVar[Path | None] = None - DEFAULT_NOTIFICATIONS_ENDPOINT: ClassVar[HttpUrl | None] = None DEFAULT_UNLOCK_TIMEOUT: ClassVar[int] = 900 DEFAULT_UNLOCK_INTERVAL: ClassVar[int] = 500 DEFAULT_WALLET_DIR: ClassVar[Path] = Path.cwd() @@ -29,10 +27,9 @@ class ExportKeysWalletParams: class BeekeeperArguments(Arguments): backtrace: Literal["yes", "no"] | None = BeekeeperArgumentsDefaults.DEFAULT_BACKTRACE - data_dir: Path = BeekeeperArgumentsDefaults.DEFAULT_DATA_DIR + data_dir: Path | None = None export_keys_wallet: ExportKeysWalletParams | None = BeekeeperArgumentsDefaults.DEFAULT_EXPORT_KEYS_WALLET log_json_rpc: Path | None = BeekeeperArgumentsDefaults.DEFAULT_LOG_JSON_RPC - notifications_endpoint: HttpUrl | None = BeekeeperArgumentsDefaults.DEFAULT_NOTIFICATIONS_ENDPOINT unlock_timeout: int | None = BeekeeperArgumentsDefaults.DEFAULT_UNLOCK_TIMEOUT wallet_dir: Path | None = BeekeeperArgumentsDefaults.DEFAULT_WALLET_DIR webserver_thread_pool_size: int | None = BeekeeperArgumentsDefaults.DEFAULT_WEBSERVER_THREAD_POOL_SIZE diff --git a/beekeepy/beekeepy/_executable/beekeeper_config.py b/beekeepy/beekeepy/_executable/beekeeper_config.py index 904ebb7c..8dd7fdac 100644 --- a/beekeepy/beekeepy/_executable/beekeeper_config.py +++ b/beekeepy/beekeepy/_executable/beekeeper_config.py @@ -4,9 +4,9 @@ from pathlib import Path # noqa: TCH003 from pydantic import Field +from beekeepy._communication import HttpUrl, WsUrl +from beekeepy._executable.abc.config import Config from beekeepy._executable.defaults import BeekeeperDefaults, ExportKeysWalletParams -from beekeepy._interface.config import Config -from beekeepy._interface.url import HttpUrl, WsUrl def http_webserver_default() -> HttpUrl: @@ -23,7 +23,6 @@ class BeekeeperConfig(Config): webserver_ws_endpoint: WsUrl | None = None webserver_ws_deflate: int = 0 webserver_thread_pool_size: int = 1 - notifications_endpoint: HttpUrl | None = BeekeeperDefaults.DEFAULT_NOTIFICATIONS_ENDPOINT backtrace: str = BeekeeperDefaults.DEFAULT_BACKTRACE plugin: list[str] = Field(default_factory=lambda: ["json_rpc", "webserver"]) export_keys_wallet: ExportKeysWalletParams | None = BeekeeperDefaults.DEFAULT_EXPORT_KEYS_WALLET diff --git a/beekeepy/beekeepy/_executable/beekeeper_executable.py b/beekeepy/beekeepy/_executable/beekeeper_executable.py index 38ebcf1b..e710c947 100644 --- a/beekeepy/beekeepy/_executable/beekeeper_executable.py +++ b/beekeepy/beekeepy/_executable/beekeeper_executable.py @@ -6,26 +6,24 @@ import tempfile from pathlib import Path from typing import TYPE_CHECKING -from beekeepy._executable.arguments.beekeeper_arguments import BeekeeperArguments, ExportKeysWalletParams +from beekeepy._executable.abc.executable import AutoCloser, Executable +from beekeepy._executable.beekeeper_arguments import BeekeeperArguments, ExportKeysWalletParams from beekeepy._executable.beekeeper_config import BeekeeperConfig from beekeepy._executable.beekeeper_executable_discovery import get_beekeeper_binary_path -from beekeepy._executable.executable import Executable -from beekeepy._interface.key_pair import KeyPair -from beekeepy._interface.url import HttpUrl -from beekeepy._runnable_handle.settings import Settings +from beekeepy._utilities.key_pair import KeyPair if TYPE_CHECKING: from loguru import Logger class BeekeeperExecutable(Executable[BeekeeperConfig, BeekeeperArguments]): - def __init__(self, settings: Settings, logger: Logger) -> None: - super().__init__( - settings.binary_path or get_beekeeper_binary_path(), settings.ensured_working_directory, logger - ) + def __init__(self, executable_path: Path | None, working_directory: Path, logger: Logger) -> None: + super().__init__(executable_path or get_beekeeper_binary_path(), working_directory, logger) def _construct_config(self) -> BeekeeperConfig: - return BeekeeperConfig(wallet_dir=self.working_directory) + config = BeekeeperConfig(wallet_dir=self.working_directory) + config.plugin.append("app_status_api") + return config def _construct_arguments(self) -> BeekeeperArguments: return BeekeeperArguments(data_dir=self.working_directory) @@ -38,17 +36,19 @@ class BeekeeperExecutable(Executable[BeekeeperConfig, BeekeeperArguments]): wallet_file_name = f"{wallet_name}.wallet" shutil.copyfile(self.working_directory / wallet_file_name, tempdir_path / wallet_file_name) bk = BeekeeperExecutable( - settings=Settings(binary_path=get_beekeeper_binary_path(), working_directory=self.working_directory), + executable_path=self.executable_path, + working_directory=self.working_directory, logger=self._logger, ) - bk.run( - blocking=True, - arguments=BeekeeperArguments( + with bk.restore_arguments( + BeekeeperArguments( data_dir=tempdir_path, - notifications_endpoint=HttpUrl("0.0.0.0:0"), export_keys_wallet=ExportKeysWalletParams(wallet_name=wallet_name, wallet_password=wallet_password), - ), - ) + ) + ): + bk.run( + blocking=True, + ) keys_path = bk.working_directory / f"{wallet_name}.keys" if extract_to is not None: @@ -57,3 +57,12 @@ class BeekeeperExecutable(Executable[BeekeeperConfig, BeekeeperArguments]): with keys_path.open("r") as file: return [KeyPair(**obj) for obj in json.load(file)] + + def run( + self, + *, + blocking: bool, + environ: dict[str, str] | None = None, + propagate_sigint: bool = True, + ) -> AutoCloser: + return self._run(blocking=blocking, environ=environ, propagate_sigint=propagate_sigint) diff --git a/beekeepy/beekeepy/_executable/defaults.py b/beekeepy/beekeepy/_executable/defaults.py index c3f2f5d5..894b2a37 100644 --- a/beekeepy/beekeepy/_executable/defaults.py +++ b/beekeepy/beekeepy/_executable/defaults.py @@ -6,7 +6,7 @@ from typing import ClassVar from pydantic import BaseModel -from beekeepy._interface.url import HttpUrl # noqa: TCH001 +from beekeepy._communication import HttpUrl # noqa: TCH001 @dataclass @@ -20,7 +20,6 @@ class BeekeeperDefaults(BaseModel): DEFAULT_DATA_DIR: ClassVar[Path] = Path.cwd() DEFAULT_EXPORT_KEYS_WALLET: ClassVar[ExportKeysWalletParams | None] = None DEFAULT_LOG_JSON_RPC: ClassVar[Path | None] = None - DEFAULT_NOTIFICATIONS_ENDPOINT: ClassVar[HttpUrl | None] = None DEFAULT_UNLOCK_TIMEOUT: ClassVar[int] = 900 DEFAULT_UNLOCK_INTERVAL: ClassVar[int] = 500 DEFAULT_WALLET_DIR: ClassVar[Path] = Path.cwd() diff --git a/beekeepy/beekeepy/_interface/__init__.py b/beekeepy/beekeepy/_interface/__init__.py index e69de29b..1a826ee5 100644 --- a/beekeepy/beekeepy/_interface/__init__.py +++ b/beekeepy/beekeepy/_interface/__init__.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from beekeepy._interface import abc +from beekeepy._interface.asynchronous.beekeeper import Beekeeper as AsyncBeekeper +from beekeepy._interface.asynchronous.session import Session as AsyncSession +from beekeepy._interface.asynchronous.wallet import UnlockedWallet as AsyncUnlockedWallet +from beekeepy._interface.asynchronous.wallet import Wallet as AsyncWallet +from beekeepy._interface.settings import InterfaceSettings +from beekeepy._interface.synchronous.beekeeper import Beekeeper +from beekeepy._interface.synchronous.session import Session +from beekeepy._interface.synchronous.wallet import UnlockedWallet, Wallet + +__all__ = [ + "abc", + "AsyncBeekeper", + "AsyncSession", + "AsyncUnlockedWallet", + "AsyncWallet", + "Beekeeper", + "InterfaceSettings", + "Session", + "UnlockedWallet", + "Wallet", +] diff --git a/beekeepy/beekeepy/_interface/abc/__init__.py b/beekeepy/beekeepy/_interface/abc/__init__.py index e69de29b..b73bcd06 100644 --- a/beekeepy/beekeepy/_interface/abc/__init__.py +++ b/beekeepy/beekeepy/_interface/abc/__init__.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from beekeepy._interface.abc.asynchronous.beekeeper import Beekeeper as AsyncBeekeeper +from beekeepy._interface.abc.asynchronous.session import Session as AsyncSession +from beekeepy._interface.abc.asynchronous.wallet import UnlockedWallet as AsyncUnlockedWallet +from beekeepy._interface.abc.asynchronous.wallet import Wallet as AsyncWallet +from beekeepy._interface.abc.packed_object import PackedAsyncBeekeeper, PackedSyncBeekeeper +from beekeepy._interface.abc.synchronous.beekeeper import Beekeeper +from beekeepy._interface.abc.synchronous.session import Session +from beekeepy._interface.abc.synchronous.wallet import UnlockedWallet, Wallet + +__all__ = [ + "AsyncBeekeeper", + "AsyncSession", + "AsyncUnlockedWallet", + "AsyncWallet", + "Beekeeper", + "PackedAsyncBeekeeper", + "PackedSyncBeekeeper", + "Session", + "UnlockedWallet", + "Wallet", +] diff --git a/beekeepy/beekeepy/_interface/abc/asynchronous/beekeeper.py b/beekeepy/beekeepy/_interface/abc/asynchronous/beekeeper.py index f1509340..0604b7bc 100644 --- a/beekeepy/beekeepy/_interface/abc/asynchronous/beekeeper.py +++ b/beekeepy/beekeepy/_interface/abc/asynchronous/beekeeper.py @@ -3,15 +3,15 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING, cast -from beekeepy._communication.settings import CommunicationSettings -from beekeepy._interface.context import ContextAsync -from beekeepy._interface.context_settings_updater import ContextSettingsUpdater -from beekeepy._runnable_handle.settings import Settings +from beekeepy._communication import CommunicationSettings +from beekeepy._interface.settings import InterfaceSettings +from beekeepy._utilities.context import ContextAsync +from beekeepy._utilities.context_settings_updater import ContextSettingsUpdater if TYPE_CHECKING: + from beekeepy._communication import HttpUrl from beekeepy._interface.abc.asynchronous.session import Session from beekeepy._interface.abc.packed_object import PackedAsyncBeekeeper - from beekeepy._interface.url import HttpUrl class Beekeeper(ContextAsync["Beekeeper"], ContextSettingsUpdater[CommunicationSettings], ABC): @@ -19,9 +19,9 @@ class Beekeeper(ContextAsync["Beekeeper"], ContextSettingsUpdater[CommunicationS async def create_session(self, *, salt: str | None = None) -> Session: ... @property - def settings(self) -> Settings: + def settings(self) -> InterfaceSettings: """Returns read-only settings.""" - return cast(Settings, self._get_copy_of_settings()) + return cast(InterfaceSettings, self._get_copy_of_settings()) @property def http_endpoint(self) -> HttpUrl: @@ -42,13 +42,13 @@ class Beekeeper(ContextAsync["Beekeeper"], ContextSettingsUpdater[CommunicationS """Detaches process and returns PID.""" @classmethod - async def factory(cls, *, settings: Settings | None = None) -> Beekeeper: + async def factory(cls, *, settings: InterfaceSettings | None = None) -> Beekeeper: from beekeepy._interface.asynchronous.beekeeper import Beekeeper as BeekeeperImplementation return await BeekeeperImplementation._factory(settings=settings) @classmethod - async def remote_factory(cls, *, url_or_settings: Settings | HttpUrl) -> Beekeeper: + async def remote_factory(cls, *, url_or_settings: InterfaceSettings | HttpUrl) -> Beekeeper: from beekeepy._interface.asynchronous.beekeeper import Beekeeper as BeekeeperImplementation return await BeekeeperImplementation._remote_factory(url_or_settings=url_or_settings) diff --git a/beekeepy/beekeepy/_interface/abc/asynchronous/session.py b/beekeepy/beekeepy/_interface/abc/asynchronous/session.py index befad86f..8d2c2e93 100644 --- a/beekeepy/beekeepy/_interface/abc/asynchronous/session.py +++ b/beekeepy/beekeepy/_interface/abc/asynchronous/session.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING, TypeAlias, overload -from beekeepy._interface.context import ContextAsync +from beekeepy._utilities.context import ContextAsync if TYPE_CHECKING: from beekeepy._interface.abc.asynchronous.wallet import UnlockedWallet, Wallet diff --git a/beekeepy/beekeepy/_interface/abc/asynchronous/wallet.py b/beekeepy/beekeepy/_interface/abc/asynchronous/wallet.py index f251972e..27726dde 100644 --- a/beekeepy/beekeepy/_interface/abc/asynchronous/wallet.py +++ b/beekeepy/beekeepy/_interface/abc/asynchronous/wallet.py @@ -3,8 +3,8 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING -from beekeepy._interface.context import ContextAsync from beekeepy._interface.wallets_common import ContainsWalletName +from beekeepy._utilities.context import ContextAsync if TYPE_CHECKING: from datetime import datetime diff --git a/beekeepy/beekeepy/_interface/abc/packed_object.py b/beekeepy/beekeepy/_interface/abc/packed_object.py index c92f7ddb..5ba364b8 100644 --- a/beekeepy/beekeepy/_interface/abc/packed_object.py +++ b/beekeepy/beekeepy/_interface/abc/packed_object.py @@ -2,17 +2,17 @@ from __future__ import annotations from typing import TYPE_CHECKING, Generic, Protocol, TypeVar -from beekeepy._interface.settings_holder import UniqueSettingsHolder -from beekeepy._runnable_handle.settings import Settings +from beekeepy._interface.settings import InterfaceSettings +from beekeepy._utilities.settings_holder import UniqueSettingsHolder if TYPE_CHECKING: + from beekeepy._communication import HttpUrl from beekeepy._interface.abc.asynchronous.beekeeper import ( Beekeeper as AsynchronousBeekeeperInterface, ) from beekeepy._interface.abc.synchronous.beekeeper import ( Beekeeper as SynchronousBeekeeperInterface, ) - from beekeepy._interface.url import HttpUrl __all__ = [ "PackedSyncBeekeeper", @@ -21,27 +21,22 @@ __all__ = [ class _SyncRemoteFactoryCallable(Protocol): - def __call__(self, *, url_or_settings: HttpUrl | Settings) -> SynchronousBeekeeperInterface: ... + def __call__(self, *, url_or_settings: HttpUrl | InterfaceSettings) -> SynchronousBeekeeperInterface: ... class _AsyncRemoteFactoryCallable(Protocol): - async def __call__(self, *, url_or_settings: HttpUrl | Settings) -> AsynchronousBeekeeperInterface: ... + async def __call__(self, *, url_or_settings: HttpUrl | InterfaceSettings) -> AsynchronousBeekeeperInterface: ... FactoryT = TypeVar("FactoryT", bound=_SyncRemoteFactoryCallable | _AsyncRemoteFactoryCallable) -class Packed(UniqueSettingsHolder[Settings], Generic[FactoryT]): +class Packed(UniqueSettingsHolder[InterfaceSettings], Generic[FactoryT]): """Allows to transfer beekeeper handle to other process.""" - def __init__(self, settings: Settings, unpack_factory: FactoryT) -> None: + def __init__(self, settings: InterfaceSettings, unpack_factory: FactoryT) -> None: super().__init__(settings=settings) self._unpack_factory = unpack_factory - self._prepare_settings_for_packing() - - def _prepare_settings_for_packing(self) -> None: - with self.update_settings() as settings: - settings.notification_endpoint = None class PackedSyncBeekeeper(Packed[_SyncRemoteFactoryCallable]): diff --git a/beekeepy/beekeepy/_interface/abc/synchronous/beekeeper.py b/beekeepy/beekeepy/_interface/abc/synchronous/beekeeper.py index 22febafd..a8068fb2 100644 --- a/beekeepy/beekeepy/_interface/abc/synchronous/beekeeper.py +++ b/beekeepy/beekeepy/_interface/abc/synchronous/beekeeper.py @@ -3,15 +3,15 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING, cast -from beekeepy._communication.settings import CommunicationSettings -from beekeepy._interface.context import ContextSync -from beekeepy._interface.context_settings_updater import ContextSettingsUpdater -from beekeepy._runnable_handle.settings import Settings +from beekeepy._communication import CommunicationSettings +from beekeepy._interface.settings import InterfaceSettings +from beekeepy._utilities.context import ContextSync +from beekeepy._utilities.context_settings_updater import ContextSettingsUpdater if TYPE_CHECKING: + from beekeepy._communication import HttpUrl from beekeepy._interface.abc.packed_object import PackedSyncBeekeeper from beekeepy._interface.abc.synchronous.session import Session - from beekeepy._interface.url import HttpUrl class Beekeeper(ContextSync["Beekeeper"], ContextSettingsUpdater[CommunicationSettings], ABC): @@ -19,9 +19,9 @@ class Beekeeper(ContextSync["Beekeeper"], ContextSettingsUpdater[CommunicationSe def create_session(self, *, salt: str | None = None) -> Session: ... @property - def settings(self) -> Settings: + def settings(self) -> InterfaceSettings: """Returns read-only settings.""" - return cast(Settings, self._get_copy_of_settings()) + return cast(InterfaceSettings, self._get_copy_of_settings()) @property def http_endpoint(self) -> HttpUrl: @@ -42,13 +42,13 @@ class Beekeeper(ContextSync["Beekeeper"], ContextSettingsUpdater[CommunicationSe """Detaches process and returns PID.""" @classmethod - def factory(cls, *, settings: Settings | None = None) -> Beekeeper: + def factory(cls, *, settings: InterfaceSettings | None = None) -> Beekeeper: from beekeepy._interface.synchronous.beekeeper import Beekeeper as BeekeeperImplementation return BeekeeperImplementation._factory(settings=settings) @classmethod - def remote_factory(cls, *, url_or_settings: Settings | HttpUrl) -> Beekeeper: + def remote_factory(cls, *, url_or_settings: InterfaceSettings | HttpUrl) -> Beekeeper: from beekeepy._interface.synchronous.beekeeper import Beekeeper as BeekeeperImplementation return BeekeeperImplementation._remote_factory(url_or_settings=url_or_settings) diff --git a/beekeepy/beekeepy/_interface/abc/synchronous/session.py b/beekeepy/beekeepy/_interface/abc/synchronous/session.py index 458b9640..2292259b 100644 --- a/beekeepy/beekeepy/_interface/abc/synchronous/session.py +++ b/beekeepy/beekeepy/_interface/abc/synchronous/session.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING, TypeAlias, overload -from beekeepy._interface.context import ContextSync +from beekeepy._utilities.context import ContextSync if TYPE_CHECKING: from beekeepy._interface.abc.synchronous.wallet import UnlockedWallet, Wallet diff --git a/beekeepy/beekeepy/_interface/abc/synchronous/wallet.py b/beekeepy/beekeepy/_interface/abc/synchronous/wallet.py index 31f07bec..51895086 100644 --- a/beekeepy/beekeepy/_interface/abc/synchronous/wallet.py +++ b/beekeepy/beekeepy/_interface/abc/synchronous/wallet.py @@ -3,8 +3,8 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING -from beekeepy._interface.context import ContextSync from beekeepy._interface.wallets_common import ContainsWalletName +from beekeepy._utilities.context import ContextSync from schemas.fields.basic import PublicKey if TYPE_CHECKING: diff --git a/beekeepy/beekeepy/_interface/asynchronous/beekeeper.py b/beekeepy/beekeepy/_interface/asynchronous/beekeeper.py index 799922a0..d195568d 100644 --- a/beekeepy/beekeepy/_interface/asynchronous/beekeeper.py +++ b/beekeepy/beekeepy/_interface/asynchronous/beekeeper.py @@ -7,11 +7,11 @@ from loguru import logger from beekeepy._interface.abc.asynchronous.beekeeper import Beekeeper as BeekeeperInterface from beekeepy._interface.abc.packed_object import PackedAsyncBeekeeper from beekeepy._interface.asynchronous.session import Session -from beekeepy._interface.delay_guard import AsyncDelayGuard -from beekeepy._interface.state_invalidator import StateInvalidator -from beekeepy._remote_handle.beekeeper import AsyncBeekeeper as AsynchronousRemoteBeekeeperHandle -from beekeepy._runnable_handle.beekeeper import AsyncBeekeeper as AsynchronousBeekeeperHandle -from beekeepy._runnable_handle.settings import Settings +from beekeepy._interface.settings import InterfaceSettings +from beekeepy._remote_handle import AsyncBeekeeperTemplate as AsynchronousRemoteBeekeeperHandle +from beekeepy._runnable_handle import AsyncBeekeeperTemplate as AsynchronousBeekeeperHandle +from beekeepy._utilities.delay_guard import AsyncDelayGuard +from beekeepy._utilities.state_invalidator import StateInvalidator from beekeepy.exceptions import ( DetachRemoteBeekeeperError, InvalidatedStateByClosingBeekeeperError, @@ -19,15 +19,14 @@ from beekeepy.exceptions import ( ) if TYPE_CHECKING: - from beekeepy._communication.settings import CommunicationSettings + from beekeepy._communication import CommunicationSettings, HttpUrl from beekeepy._interface.abc.asynchronous.session import ( Session as SessionInterface, ) - from beekeepy._interface.url import HttpUrl class Beekeeper(BeekeeperInterface, StateInvalidator): - def __init__(self, *args: Any, handle: AsynchronousRemoteBeekeeperHandle, **kwargs: Any) -> None: + def __init__(self, *args: Any, handle: AsynchronousRemoteBeekeeperHandle[InterfaceSettings], **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.__instance = handle self.__guard = AsyncDelayGuard() @@ -48,7 +47,7 @@ class Beekeeper(BeekeeperInterface, StateInvalidator): self.__default_session = self.__create_session((await self._get_instance().session).token) return self.__default_session - def _get_instance(self) -> AsynchronousRemoteBeekeeperHandle: + def _get_instance(self) -> AsynchronousRemoteBeekeeperHandle[InterfaceSettings]: return self.__instance @StateInvalidator.empty_call_after_invalidation(None) @@ -80,28 +79,38 @@ class Beekeeper(BeekeeperInterface, StateInvalidator): return PackedAsyncBeekeeper(settings=self.settings, unpack_factory=Beekeeper._remote_factory) @classmethod - async def _factory(cls, *, settings: Settings | None = None) -> BeekeeperInterface: - settings = settings or Settings() - handle = AsynchronousBeekeeperHandle(settings=settings, logger=logger) + async def _factory(cls, *, settings: InterfaceSettings | None = None) -> BeekeeperInterface: + settings = settings or InterfaceSettings() + handle = cls.__create_local_handle(settings=settings) handle.run() return cls(handle=handle) @classmethod - async def _remote_factory(cls, *, url_or_settings: Settings | HttpUrl) -> BeekeeperInterface: - if isinstance(url_or_settings, Settings): + async def _remote_factory(cls, *, url_or_settings: InterfaceSettings | HttpUrl) -> BeekeeperInterface: + if isinstance(url_or_settings, InterfaceSettings): assert ( url_or_settings.http_endpoint is not None ), "Settings.http_endpoint has to be set when passing to remote_factory" - settings = url_or_settings if isinstance(url_or_settings, Settings) else Settings(http_endpoint=url_or_settings) + settings = ( + url_or_settings + if isinstance(url_or_settings, InterfaceSettings) + else InterfaceSettings(http_endpoint=url_or_settings) + ) handle = AsynchronousRemoteBeekeeperHandle(settings=settings) cls.__apply_existing_session_token(settings=settings, handle=handle) return cls(handle=handle) @classmethod - def __apply_existing_session_token(cls, settings: Settings, handle: AsynchronousRemoteBeekeeperHandle) -> None: + def __apply_existing_session_token( + cls, settings: InterfaceSettings, handle: AsynchronousRemoteBeekeeperHandle[InterfaceSettings] + ) -> None: if settings.use_existing_session: handle.set_session_token(settings.use_existing_session) + @classmethod + def __create_local_handle(cls, settings: InterfaceSettings) -> AsynchronousBeekeeperHandle[InterfaceSettings]: + return AsynchronousBeekeeperHandle(settings=settings, logger=logger) + async def _aenter(self) -> BeekeeperInterface: return self diff --git a/beekeepy/beekeepy/_interface/asynchronous/session.py b/beekeepy/beekeepy/_interface/asynchronous/session.py index 6e808935..470f58d0 100644 --- a/beekeepy/beekeepy/_interface/asynchronous/session.py +++ b/beekeepy/beekeepy/_interface/asynchronous/session.py @@ -9,8 +9,8 @@ from beekeepy._interface.asynchronous.wallet import ( UnlockedWallet, Wallet, ) -from beekeepy._interface.state_invalidator import StateInvalidator from beekeepy._interface.validators import validate_digest, validate_public_keys, validate_timeout +from beekeepy._utilities.state_invalidator import StateInvalidator from beekeepy.exceptions import ( InvalidatedStateByClosingSessionError, InvalidWalletError, @@ -27,8 +27,9 @@ if TYPE_CHECKING: from beekeepy._interface.abc.asynchronous.wallet import ( Wallet as WalletInterface, ) - from beekeepy._interface.delay_guard import AsyncDelayGuard - from beekeepy._remote_handle.beekeeper import AsyncBeekeeper as AsynchronousRemoteBeekeeperHandle + from beekeepy._interface.settings import InterfaceSettings + from beekeepy._remote_handle import AsyncBeekeeperTemplate as AsynchronousRemoteBeekeeperHandle + from beekeepy._utilities.delay_guard import AsyncDelayGuard from schemas.apis.beekeeper_api import GetInfo from schemas.fields.basic import PublicKey from schemas.fields.hex import Signature @@ -38,7 +39,7 @@ class Session(SessionInterface, StateInvalidator): def __init__( self, *args: Any, - beekeeper: AsynchronousRemoteBeekeeperHandle, + beekeeper: AsynchronousRemoteBeekeeperHandle[InterfaceSettings], guard: AsyncDelayGuard, use_session_token: str | None = None, default_session_close_callback: Callable[[], None] | None = None, diff --git a/beekeepy/beekeepy/_interface/asynchronous/wallet.py b/beekeepy/beekeepy/_interface/asynchronous/wallet.py index 761cb085..20173c12 100644 --- a/beekeepy/beekeepy/_interface/asynchronous/wallet.py +++ b/beekeepy/beekeepy/_interface/asynchronous/wallet.py @@ -8,11 +8,12 @@ from beekeepy._interface.abc.asynchronous.wallet import ( from beekeepy._interface.abc.asynchronous.wallet import ( Wallet as WalletInterface, ) -from beekeepy._interface.delay_guard import AsyncDelayGuard +from beekeepy._interface.settings import InterfaceSettings from beekeepy._interface.validators import validate_digest, validate_private_keys, validate_public_keys from beekeepy._interface.wallets_common import WalletCommons -from beekeepy._remote_handle.beekeeper import AsyncBeekeeper as AsyncRemoteBeekeeper -from beekeepy._runnable_handle.callbacks_protocol import AsyncWalletLocked +from beekeepy._remote_handle import AsyncBeekeeperTemplate as AsyncRemoteBeekeeper +from beekeepy._runnable_handle import AsyncWalletLocked +from beekeepy._utilities.delay_guard import AsyncDelayGuard from beekeepy.exceptions import ( InvalidPasswordError, InvalidPrivateKeyError, @@ -29,7 +30,9 @@ if TYPE_CHECKING: from schemas.fields.hex import Signature -class Wallet(WalletCommons[AsyncRemoteBeekeeper, AsyncWalletLocked, AsyncDelayGuard], WalletInterface): +class Wallet( + WalletCommons[AsyncRemoteBeekeeper[InterfaceSettings], AsyncWalletLocked, AsyncDelayGuard], WalletInterface +): @property async def public_keys(self) -> list[PublicKey]: return [ diff --git a/beekeepy/beekeepy/_interface/settings.py b/beekeepy/beekeepy/_interface/settings.py new file mode 100644 index 00000000..701919db --- /dev/null +++ b/beekeepy/beekeepy/_interface/settings.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from beekeepy._runnable_handle import RunnableHandleSettings + + +class InterfaceSettings(RunnableHandleSettings): + """Settings for beekeeper interface.""" diff --git a/beekeepy/beekeepy/_interface/synchronous/beekeeper.py b/beekeepy/beekeepy/_interface/synchronous/beekeeper.py index 3761fe38..e287b00b 100644 --- a/beekeepy/beekeepy/_interface/synchronous/beekeeper.py +++ b/beekeepy/beekeepy/_interface/synchronous/beekeeper.py @@ -6,12 +6,12 @@ from loguru import logger from beekeepy._interface.abc.packed_object import PackedSyncBeekeeper from beekeepy._interface.abc.synchronous.beekeeper import Beekeeper as BeekeeperInterface -from beekeepy._interface.delay_guard import SyncDelayGuard -from beekeepy._interface.state_invalidator import StateInvalidator +from beekeepy._interface.settings import InterfaceSettings from beekeepy._interface.synchronous.session import Session -from beekeepy._remote_handle.beekeeper import Beekeeper as SynchronousRemoteBeekeeperHandle -from beekeepy._runnable_handle.beekeeper import Beekeeper as SynchronousBeekeeperHandle -from beekeepy._runnable_handle.settings import Settings +from beekeepy._remote_handle import BeekeeperTemplate as SynchronousRemoteBeekeeperHandle +from beekeepy._runnable_handle import BeekeeperTemplate as SynchronousBeekeeperHandle +from beekeepy._utilities.delay_guard import SyncDelayGuard +from beekeepy._utilities.state_invalidator import StateInvalidator from beekeepy.exceptions import ( DetachRemoteBeekeeperError, InvalidatedStateByClosingBeekeeperError, @@ -19,15 +19,14 @@ from beekeepy.exceptions import ( ) if TYPE_CHECKING: - from beekeepy._communication.settings import CommunicationSettings + from beekeepy._communication import CommunicationSettings, HttpUrl from beekeepy._interface.abc.synchronous.session import ( Session as SessionInterface, ) - from beekeepy._interface.url import HttpUrl class Beekeeper(BeekeeperInterface, StateInvalidator): - def __init__(self, *args: Any, handle: SynchronousRemoteBeekeeperHandle, **kwargs: Any) -> None: + def __init__(self, *args: Any, handle: SynchronousRemoteBeekeeperHandle[InterfaceSettings], **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.__instance = handle self.__guard = SyncDelayGuard() @@ -48,7 +47,7 @@ class Beekeeper(BeekeeperInterface, StateInvalidator): self.__default_session = self.__create_session(self._get_instance().session.token, default_session=True) return self.__default_session - def _get_instance(self) -> SynchronousRemoteBeekeeperHandle: + def _get_instance(self) -> SynchronousRemoteBeekeeperHandle[InterfaceSettings]: return self.__instance @StateInvalidator.empty_call_after_invalidation(None) @@ -80,28 +79,38 @@ class Beekeeper(BeekeeperInterface, StateInvalidator): return PackedSyncBeekeeper(settings=self.settings, unpack_factory=Beekeeper._remote_factory) @classmethod - def _factory(cls, *, settings: Settings | None = None) -> BeekeeperInterface: - settings = settings or Settings() - handle = SynchronousBeekeeperHandle(settings=settings, logger=logger) + def _factory(cls, *, settings: InterfaceSettings | None = None) -> BeekeeperInterface: + settings = settings or InterfaceSettings() + handle = cls.__create_local_handle(settings=settings) handle.run() return cls(handle=handle) @classmethod - def _remote_factory(cls, *, url_or_settings: Settings | HttpUrl) -> BeekeeperInterface: - if isinstance(url_or_settings, Settings): + def _remote_factory(cls, *, url_or_settings: InterfaceSettings | HttpUrl) -> BeekeeperInterface: + if isinstance(url_or_settings, InterfaceSettings): assert ( url_or_settings.http_endpoint is not None ), "Settings.http_endpoint has to be set when passing to remote_factory" - settings = url_or_settings if isinstance(url_or_settings, Settings) else Settings(http_endpoint=url_or_settings) + settings = ( + url_or_settings + if isinstance(url_or_settings, InterfaceSettings) + else InterfaceSettings(http_endpoint=url_or_settings) + ) handle = SynchronousRemoteBeekeeperHandle(settings=settings) cls.__apply_existing_session_token(settings=settings, handle=handle) return cls(handle=handle) @classmethod - def __apply_existing_session_token(cls, settings: Settings, handle: SynchronousRemoteBeekeeperHandle) -> None: + def __apply_existing_session_token( + cls, settings: InterfaceSettings, handle: SynchronousRemoteBeekeeperHandle[InterfaceSettings] + ) -> None: if settings.use_existing_session: handle.set_session_token(settings.use_existing_session) + @classmethod + def __create_local_handle(cls, settings: InterfaceSettings) -> SynchronousBeekeeperHandle[InterfaceSettings]: + return SynchronousBeekeeperHandle(settings=settings, logger=logger) + def _enter(self) -> BeekeeperInterface: return self diff --git a/beekeepy/beekeepy/_interface/synchronous/session.py b/beekeepy/beekeepy/_interface/synchronous/session.py index 3e616a05..5b9e559d 100644 --- a/beekeepy/beekeepy/_interface/synchronous/session.py +++ b/beekeepy/beekeepy/_interface/synchronous/session.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING, Any, Callable from beekeepy._interface.abc.synchronous.session import Password from beekeepy._interface.abc.synchronous.session import Session as SessionInterface -from beekeepy._interface.state_invalidator import StateInvalidator from beekeepy._interface.synchronous.wallet import ( UnlockedWallet, Wallet, ) from beekeepy._interface.validators import validate_digest, validate_public_keys, validate_timeout +from beekeepy._utilities.state_invalidator import StateInvalidator from beekeepy.exceptions import ( InvalidatedStateByClosingSessionError, InvalidWalletError, @@ -26,8 +26,9 @@ if TYPE_CHECKING: from beekeepy._interface.abc.synchronous.wallet import ( Wallet as WalletInterface, ) - from beekeepy._interface.delay_guard import SyncDelayGuard - from beekeepy._remote_handle.beekeeper import Beekeeper as SyncRemoteBeekeeper + from beekeepy._interface.settings import InterfaceSettings + from beekeepy._remote_handle import BeekeeperTemplate as SyncRemoteBeekeeper + from beekeepy._utilities.delay_guard import SyncDelayGuard from schemas.apis.beekeeper_api import GetInfo from schemas.fields.basic import PublicKey from schemas.fields.hex import Signature @@ -37,7 +38,7 @@ class Session(SessionInterface, StateInvalidator): def __init__( self, *args: Any, - beekeeper: SyncRemoteBeekeeper, + beekeeper: SyncRemoteBeekeeper[InterfaceSettings], guard: SyncDelayGuard, use_session_token: str | None = None, default_session_close_callback: Callable[[], None] | None = None, diff --git a/beekeepy/beekeepy/_interface/synchronous/wallet.py b/beekeepy/beekeepy/_interface/synchronous/wallet.py index d23ae316..81ffec89 100644 --- a/beekeepy/beekeepy/_interface/synchronous/wallet.py +++ b/beekeepy/beekeepy/_interface/synchronous/wallet.py @@ -8,11 +8,12 @@ from beekeepy._interface.abc.synchronous.wallet import ( from beekeepy._interface.abc.synchronous.wallet import ( Wallet as WalletInterface, ) -from beekeepy._interface.delay_guard import SyncDelayGuard +from beekeepy._interface.settings import InterfaceSettings from beekeepy._interface.validators import validate_private_keys, validate_public_keys from beekeepy._interface.wallets_common import WalletCommons -from beekeepy._remote_handle.beekeeper import Beekeeper as SyncRemoteBeekeeper -from beekeepy._runnable_handle.callbacks_protocol import SyncWalletLocked +from beekeepy._remote_handle import BeekeeperTemplate as SyncRemoteBeekeeper +from beekeepy._runnable_handle import SyncWalletLocked +from beekeepy._utilities.delay_guard import SyncDelayGuard from beekeepy.exceptions import ( InvalidPasswordError, InvalidPrivateKeyError, @@ -29,7 +30,7 @@ if TYPE_CHECKING: from schemas.fields.hex import Signature -class Wallet(WalletCommons[SyncRemoteBeekeeper, SyncWalletLocked, SyncDelayGuard], WalletInterface): +class Wallet(WalletCommons[SyncRemoteBeekeeper[InterfaceSettings], SyncWalletLocked, SyncDelayGuard], WalletInterface): @property def public_keys(self) -> list[PublicKey]: return [ diff --git a/beekeepy/beekeepy/_interface/wallets_common.py b/beekeepy/beekeepy/_interface/wallets_common.py index 2750323c..328655f4 100644 --- a/beekeepy/beekeepy/_interface/wallets_common.py +++ b/beekeepy/beekeepy/_interface/wallets_common.py @@ -6,11 +6,12 @@ from asyncio import iscoroutinefunction from functools import wraps from typing import TYPE_CHECKING, Any, Generic, NoReturn, ParamSpec, TypeVar, overload -from beekeepy._interface.delay_guard import AsyncDelayGuard, SyncDelayGuard -from beekeepy._interface.state_invalidator import StateInvalidator -from beekeepy._remote_handle.beekeeper import AsyncBeekeeper as AsyncRemoteBeekeeper -from beekeepy._remote_handle.beekeeper import Beekeeper as SyncRemoteBeekeeper -from beekeepy._runnable_handle.callbacks_protocol import AsyncWalletLocked, SyncWalletLocked +from beekeepy._interface.settings import InterfaceSettings +from beekeepy._remote_handle import AsyncBeekeeperTemplate as AsyncRemoteBeekeeper +from beekeepy._remote_handle import BeekeeperTemplate as SyncRemoteBeekeeper +from beekeepy._runnable_handle import AsyncWalletLocked, SyncWalletLocked +from beekeepy._utilities.delay_guard import AsyncDelayGuard, SyncDelayGuard +from beekeepy._utilities.state_invalidator import StateInvalidator from beekeepy.exceptions import WalletIsLockedError if TYPE_CHECKING: @@ -20,7 +21,9 @@ if TYPE_CHECKING: P = ParamSpec("P") ResultT = TypeVar("ResultT") -BeekeeperT = TypeVar("BeekeeperT", bound=SyncRemoteBeekeeper | AsyncRemoteBeekeeper) +BeekeeperT = TypeVar( + "BeekeeperT", bound=SyncRemoteBeekeeper[InterfaceSettings] | AsyncRemoteBeekeeper[InterfaceSettings] +) CallbackT = TypeVar("CallbackT", bound=AsyncWalletLocked | SyncWalletLocked) GuardT = TypeVar("GuardT", bound=SyncDelayGuard | AsyncDelayGuard) @@ -132,7 +135,9 @@ class WalletCommons(ContainsWalletName, StateInvalidator, Generic[BeekeeperT, Ca @wraps(wrapped_function) async def async_impl(*args: P.args, **kwrags: P.kwargs) -> ResultT: - this: WalletCommons[AsyncRemoteBeekeeper, AsyncWalletLocked, AsyncDelayGuard] = args[0] # type: ignore[assignment] + this: WalletCommons[AsyncRemoteBeekeeper[InterfaceSettings], AsyncWalletLocked, AsyncDelayGuard] = args[ + 0 + ] # type: ignore[assignment] await this._async_call_callback_if_locked(wallet_name=this.name, token=this.session_token) return await wrapped_function(*args, **kwrags) # type: ignore[no-any-return] @@ -140,7 +145,7 @@ class WalletCommons(ContainsWalletName, StateInvalidator, Generic[BeekeeperT, Ca @wraps(wrapped_function) def sync_impl(*args: P.args, **kwrags: P.kwargs) -> ResultT: - this: WalletCommons[SyncRemoteBeekeeper, SyncWalletLocked, SyncDelayGuard] = args[0] # type: ignore[assignment] + this: WalletCommons[SyncRemoteBeekeeper[InterfaceSettings], SyncWalletLocked, SyncDelayGuard] = args[0] # type: ignore[assignment] this._sync_call_callback_if_locked(wallet_name=this.name, token=this.session_token) return wrapped_function(*args, **kwrags) # type: ignore[return-value] diff --git a/beekeepy/beekeepy/_remote_handle/__init__.py b/beekeepy/beekeepy/_remote_handle/__init__.py index e69de29b..e8d89f4d 100644 --- a/beekeepy/beekeepy/_remote_handle/__init__.py +++ b/beekeepy/beekeepy/_remote_handle/__init__.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from beekeepy._remote_handle.abc.batch_handle import ApiFactory, AsyncBatchHandle, SyncBatchHandle +from beekeepy._remote_handle.abc.handle import AbstractAsyncHandle, AbstractSyncHandle +from beekeepy._remote_handle.app_status_probe import AppStatusProbe +from beekeepy._remote_handle.beekeeper import AsyncBeekeeper as AsyncBeekeeperTemplate +from beekeepy._remote_handle.beekeeper import Beekeeper as BeekeeperTemplate +from beekeepy._remote_handle.beekeeper import _AsyncSessionBatchHandle as AsyncBeekeeprBatchHandle +from beekeepy._remote_handle.beekeeper import _SyncSessionBatchHandle as SyncBeekeeprBatchHandle +from beekeepy._remote_handle.settings import RemoteHandleSettings + +AsyncBeekeeper = AsyncBeekeeperTemplate[RemoteHandleSettings] +Beekeeper = BeekeeperTemplate[RemoteHandleSettings] + +__all__ = [ + "AbstractAsyncHandle", + "AbstractSyncHandle", + "ApiFactory", + "AppStatusProbe", + "AsyncBatchHandle", + "AsyncBeekeeper", + "AsyncBeekeeperTemplate", + "AsyncBeekeeprBatchHandle", + "Beekeeper", + "BeekeeperTemplate", + "RemoteHandleSettings", + "SyncBatchHandle", + "SyncBatchHandle", + "SyncBeekeeprBatchHandle", +] diff --git a/beekeepy/beekeepy/_remote_handle/batch_handle.py b/beekeepy/beekeepy/_remote_handle/abc/batch_handle.py similarity index 94% rename from beekeepy/beekeepy/_remote_handle/batch_handle.py rename to beekeepy/beekeepy/_remote_handle/abc/batch_handle.py index a8e364f4..e0ad80a1 100644 --- a/beekeepy/beekeepy/_remote_handle/batch_handle.py +++ b/beekeepy/beekeepy/_remote_handle/abc/batch_handle.py @@ -6,8 +6,9 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Generic, TypeVar from beekeepy import exceptions -from beekeepy._interface.context import ContextAsync, ContextSync, EnterReturnT -from beekeepy._remote_handle.build_json_rpc_call import build_json_rpc_call +from beekeepy._apis.abc import AsyncSendable, SyncSendable +from beekeepy._utilities.build_json_rpc_call import build_json_rpc_call +from beekeepy._utilities.context import ContextAsync, ContextSync, EnterReturnT from schemas.jsonrpc import ExpectResultT, JSONRPCResult, get_response_model if TYPE_CHECKING: @@ -15,8 +16,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from beekeepy._communication.abc.overseer import AbstractOverseer - from beekeepy._interface.url import HttpUrl + from beekeepy._communication import AbstractOverseer, HttpUrl class _DelayedResponseWrapper: @@ -207,7 +207,7 @@ OwnerT = TypeVar("OwnerT") ApiFactory = Callable[[OwnerT], ApiT] -class SyncBatchHandle(_BatchHandle["SyncBatchHandle"], Generic[ApiT]): # type: ignore[type-arg] +class SyncBatchHandle(_BatchHandle["SyncBatchHandle"], SyncSendable, Generic[ApiT]): # type: ignore[type-arg] def __init__( self, url: HttpUrl, @@ -224,7 +224,7 @@ class SyncBatchHandle(_BatchHandle["SyncBatchHandle"], Generic[ApiT]): # type: return self._impl_handle_request(endpoint, params, expect_type=expected_type) # type: ignore[arg-type] -class AsyncBatchHandle(_BatchHandle["AsyncBatchHandle"], Generic[ApiT]): # type: ignore[type-arg] +class AsyncBatchHandle(_BatchHandle["AsyncBatchHandle"], AsyncSendable, Generic[ApiT]): # type: ignore[type-arg] def __init__( self, url: HttpUrl, diff --git a/beekeepy/beekeepy/_remote_handle/abc/handle.py b/beekeepy/beekeepy/_remote_handle/abc/handle.py index dc77ea88..3005e4f1 100644 --- a/beekeepy/beekeepy/_remote_handle/abc/handle.py +++ b/beekeepy/beekeepy/_remote_handle/abc/handle.py @@ -5,36 +5,40 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar from loguru import logger as loguru_logger -from beekeepy._communication.aiohttp_communicator import AioHttpCommunicator -from beekeepy._communication.request_communicator import RequestCommunicator -from beekeepy._interface.context import SelfContextAsync, SelfContextSync -from beekeepy._interface.settings_holder import UniqueSettingsHolder -from beekeepy._interface.stopwatch import Stopwatch -from beekeepy._remote_handle.build_json_rpc_call import build_json_rpc_call -from beekeepy._remote_handle.settings import Settings +from beekeepy._apis.abc import ( + AbstractAsyncApiCollection, + AbstractSyncApiCollection, +) +from beekeepy._apis.abc.sendable import AsyncSendable, SyncSendable +from beekeepy._communication import AioHttpCommunicator, RequestCommunicator +from beekeepy._remote_handle.settings import RemoteHandleSettings +from beekeepy._utilities.build_json_rpc_call import build_json_rpc_call +from beekeepy._utilities.context import SelfContextAsync, SelfContextSync +from beekeepy._utilities.settings_holder import UniqueSettingsHolder +from beekeepy._utilities.stopwatch import Stopwatch from beekeepy.exceptions import CommunicationError from schemas.jsonrpc import ExpectResultT, JSONRPCResult, get_response_model if TYPE_CHECKING: from loguru import Logger - from beekeepy._communication.abc.communicator import AbstractCommunicator - from beekeepy._communication.abc.overseer import AbstractOverseer - from beekeepy._interface.url import HttpUrl - from beekeepy._remote_handle.batch_handle import AsyncBatchHandle, SyncBatchHandle + from beekeepy._communication import AbstractCommunicator, AbstractOverseer, HttpUrl + from beekeepy._remote_handle.abc.batch_handle import AsyncBatchHandle, SyncBatchHandle from beekeepy.exceptions import Json +RemoteSettingsT = TypeVar("RemoteSettingsT", bound=RemoteHandleSettings) -ApiT = TypeVar("ApiT") +ApiT = TypeVar("ApiT", bound=AbstractAsyncApiCollection | AbstractSyncApiCollection) -class AbstractHandle(UniqueSettingsHolder[Settings], ABC, Generic[ApiT]): + +class AbstractHandle(UniqueSettingsHolder[RemoteSettingsT], ABC, Generic[RemoteSettingsT, ApiT]): """Provides basic interface for all network handles.""" def __init__( self, *args: Any, - settings: Settings, + settings: RemoteSettingsT, logger: Logger | None = None, **kwargs: Any, ) -> None: @@ -63,6 +67,10 @@ class AbstractHandle(UniqueSettingsHolder[Settings], ABC, Generic[ApiT]): with self.update_settings() as settings: settings.http_endpoint = value + @property + def apis(self) -> ApiT: + return self.__api + @property def api(self) -> ApiT: return self.__api @@ -138,14 +146,14 @@ class AbstractHandle(UniqueSettingsHolder[Settings], ABC, Generic[ApiT]): ) -class AbstractAsyncHandle(AbstractHandle[ApiT], SelfContextAsync, ABC): +class AbstractAsyncHandle(AbstractHandle[RemoteSettingsT, ApiT], SelfContextAsync, AsyncSendable, ABC): """Base class for service handlers that uses asynchronous communication.""" async def _async_send( self, *, endpoint: str, params: str, expected_type: type[ExpectResultT] ) -> JSONRPCResult[ExpectResultT]: """Sends data asynchronously to handled service basing on jsonrpc.""" - from beekeepy._interface.error_logger import ErrorLogger + from beekeepy._utilities.error_logger import ErrorLogger request = build_json_rpc_call(method=endpoint, params=params) self._log_request(request) @@ -168,12 +176,12 @@ class AbstractAsyncHandle(AbstractHandle[ApiT], SelfContextAsync, ABC): self.teardown() -class AbstractSyncHandle(AbstractHandle[ApiT], SelfContextSync, ABC): +class AbstractSyncHandle(AbstractHandle[RemoteSettingsT, ApiT], SelfContextSync, SyncSendable, ABC): """Base class for service handlers that uses synchronous communication.""" def _send(self, *, endpoint: str, params: str, expected_type: type[ExpectResultT]) -> JSONRPCResult[ExpectResultT]: """Sends data synchronously to handled service basing on jsonrpc.""" - from beekeepy._interface.error_logger import ErrorLogger + from beekeepy._utilities.error_logger import ErrorLogger request = build_json_rpc_call(method=endpoint, params=params) self._log_request(request) diff --git a/beekeepy/beekeepy/_remote_handle/api/__init__.py b/beekeepy/beekeepy/_remote_handle/api/__init__.py deleted file mode 100644 index 6d4be492..00000000 --- a/beekeepy/beekeepy/_remote_handle/api/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import annotations - -from beekeepy._remote_handle.api.async_api import BeekeeperApi as AsyncBeekeeperApi -from beekeepy._remote_handle.api.sync_api import BeekeeperApi as SyncBeekeeperApi - -__all__ = ["AsyncBeekeeperApi", "SyncBeekeeperApi"] diff --git a/beekeepy/beekeepy/_remote_handle/api/api_collection.py b/beekeepy/beekeepy/_remote_handle/api/api_collection.py deleted file mode 100644 index 37b2a0ff..00000000 --- a/beekeepy/beekeepy/_remote_handle/api/api_collection.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from beekeepy._remote_handle.abc.api_collection import ( - AbstractAsyncApiCollection, - AbstractSyncApiCollection, -) -from beekeepy._remote_handle.api import AsyncBeekeeperApi, SyncBeekeeperApi - -if TYPE_CHECKING: - from beekeepy._remote_handle.beekeeper import ( - AsyncBeekeeper, - Beekeeper, - _AsyncSessionBatchHandle, - _SyncSessionBatchHandle, - ) - - -class BeekeeperAsyncApiCollection(AbstractAsyncApiCollection): - """Beekeepers collection of available apis in async version.""" - - _owner: AsyncBeekeeper | _AsyncSessionBatchHandle - - def __init__(self, owner: AsyncBeekeeper | _AsyncSessionBatchHandle) -> None: - super().__init__(owner) - self.beekeeper = AsyncBeekeeperApi(owner=self._owner) - self.beekeeper_api = self.beekeeper - - -class BeekeeperSyncApiCollection(AbstractSyncApiCollection): - """Beekeepers collection of available apis in async version.""" - - _owner: Beekeeper | _SyncSessionBatchHandle - - def __init__(self, owner: Beekeeper | _SyncSessionBatchHandle) -> None: - super().__init__(owner) - self.beekeeper = SyncBeekeeperApi(owner=self._owner) - self.beekeeper_api = self.beekeeper diff --git a/beekeepy/beekeepy/_remote_handle/app_status_probe.py b/beekeepy/beekeepy/_remote_handle/app_status_probe.py new file mode 100644 index 00000000..432901fb --- /dev/null +++ b/beekeepy/beekeepy/_remote_handle/app_status_probe.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from beekeepy._apis import AppStatusProbeSyncApiCollection +from beekeepy._remote_handle.abc.handle import AbstractSyncHandle +from beekeepy._remote_handle.settings import RemoteHandleSettings + +if TYPE_CHECKING: + from beekeepy._apis import SyncAppStatusApi + from beekeepy._remote_handle.abc.batch_handle import SyncBatchHandle + + +class AppStatusProbe(AbstractSyncHandle[RemoteHandleSettings, AppStatusProbeSyncApiCollection]): + """Synchronous handle for probing.""" + + def _construct_api(self) -> AppStatusProbeSyncApiCollection: + return AppStatusProbeSyncApiCollection(owner=self) + + @property + def api(self) -> SyncAppStatusApi: # type: ignore[override] + return self.apis.app_status + + def _target_service(self) -> str: + return "app_status_probe" + + def batch(self, *, delay_error_on_data_access: bool = False) -> SyncBatchHandle[AppStatusProbeSyncApiCollection]: + raise NotImplementedError diff --git a/beekeepy/beekeepy/_remote_handle/beekeeper.py b/beekeepy/beekeepy/_remote_handle/beekeeper.py index 4d077cde..62483d26 100644 --- a/beekeepy/beekeepy/_remote_handle/beekeeper.py +++ b/beekeepy/beekeepy/_remote_handle/beekeeper.py @@ -3,21 +3,24 @@ from __future__ import annotations import uuid from typing import TYPE_CHECKING, Any, NoReturn -from beekeepy._interface._sanitize import sanitize -from beekeepy._remote_handle.abc.handle import AbstractAsyncHandle, AbstractSyncHandle -from beekeepy._remote_handle.api.api_collection import ( +from beekeepy._apis import ( + AsyncBeekeeperApi, BeekeeperAsyncApiCollection, BeekeeperSyncApiCollection, + SyncBeekeeperApi, ) -from beekeepy._remote_handle.api.session_holder import AsyncSessionHolder, SyncSessionHolder -from beekeepy._remote_handle.batch_handle import ApiFactory, AsyncBatchHandle, SyncBatchHandle +from beekeepy._apis.abc import ( + AsyncSessionHolder, + SyncSessionHolder, +) +from beekeepy._remote_handle.abc.batch_handle import ApiFactory, AsyncBatchHandle, SyncBatchHandle +from beekeepy._remote_handle.abc.handle import AbstractAsyncHandle, AbstractSyncHandle, RemoteSettingsT +from beekeepy._utilities.sanitize import sanitize if TYPE_CHECKING: from typing_extensions import Self - from beekeepy._communication.abc.overseer import AbstractOverseer - from beekeepy._interface.url import HttpUrl - from beekeepy._remote_handle.api import AsyncBeekeeperApi, SyncBeekeeperApi + from beekeepy._communication import AbstractOverseer, HttpUrl from beekeepy.exceptions import Json _handle_target_service_name = "beekeeper" @@ -67,15 +70,19 @@ class _AsyncSessionBatchHandle(AsyncBatchHandle[BeekeeperAsyncApiCollection], As _raise_acquire_not_possible() -class Beekeeper(AbstractSyncHandle[BeekeeperSyncApiCollection], SyncSessionHolder): +class Beekeeper(AbstractSyncHandle[RemoteSettingsT, BeekeeperSyncApiCollection], SyncSessionHolder): """Synchronous handle for beekeeper service communication.""" def _construct_api(self) -> BeekeeperSyncApiCollection: return BeekeeperSyncApiCollection(owner=self) + @property + def apis(self) -> BeekeeperSyncApiCollection: + return super().api + @property def api(self) -> SyncBeekeeperApi: # type: ignore[override] - return super().api.beekeeper + return self.apis.beekeeper def _target_service(self) -> str: return _handle_target_service_name @@ -99,15 +106,19 @@ class Beekeeper(AbstractSyncHandle[BeekeeperSyncApiCollection], SyncSessionHolde return sanitize(data) -class AsyncBeekeeper(AbstractAsyncHandle[BeekeeperAsyncApiCollection], AsyncSessionHolder): +class AsyncBeekeeper(AbstractAsyncHandle[RemoteSettingsT, BeekeeperAsyncApiCollection], AsyncSessionHolder): """Asynchronous handle for beekeeper service communication.""" def _construct_api(self) -> BeekeeperAsyncApiCollection: return BeekeeperAsyncApiCollection(owner=self) + @property + def apis(self) -> BeekeeperAsyncApiCollection: + return super().api + @property def api(self) -> AsyncBeekeeperApi: # type: ignore[override] - return super().api.beekeeper + return self.apis.beekeeper def _target_service(self) -> str: return _handle_target_service_name diff --git a/beekeepy/beekeepy/_remote_handle/settings.py b/beekeepy/beekeepy/_remote_handle/settings.py index ede77a6a..470e6c1a 100644 --- a/beekeepy/beekeepy/_remote_handle/settings.py +++ b/beekeepy/beekeepy/_remote_handle/settings.py @@ -2,14 +2,16 @@ from __future__ import annotations from typing import ClassVar -from beekeepy._communication.abc.communicator import AbstractCommunicator # noqa: TCH001 -from beekeepy._communication.abc.overseer import AbstractOverseer -from beekeepy._communication.overseers import CommonOverseer -from beekeepy._communication.settings import CommunicationSettings -from beekeepy._interface.url import HttpUrl # noqa: TCH001 +from beekeepy._communication import ( + AbstractCommunicator, + AbstractOverseer, + CommonOverseer, + CommunicationSettings, + HttpUrl, +) -class Settings(CommunicationSettings): +class RemoteHandleSettings(CommunicationSettings): class Defaults(CommunicationSettings.Defaults): OVERSEER: ClassVar[type[AbstractOverseer]] = CommonOverseer diff --git a/beekeepy/beekeepy/_runnable_handle/__init__.py b/beekeepy/beekeepy/_runnable_handle/__init__.py index 3e4206d5..65e1c9a6 100644 --- a/beekeepy/beekeepy/_runnable_handle/__init__.py +++ b/beekeepy/beekeepy/_runnable_handle/__init__.py @@ -1,6 +1,21 @@ from __future__ import annotations -from beekeepy._runnable_handle.beekeeper import AsyncBeekeeper, Beekeeper -from beekeepy._runnable_handle.notification_handler_base import BeekeeperNotificationHandler +from beekeepy._runnable_handle.beekeeper import AsyncBeekeeper as AsyncBeekeeperTemplate +from beekeepy._runnable_handle.beekeeper import Beekeeper as BeekeeperTemplate +from beekeepy._runnable_handle.callbacks_protocol import AsyncWalletLocked, SyncWalletLocked +from beekeepy._runnable_handle.close_already_running_beekeeper import close_already_running_beekeeper +from beekeepy._runnable_handle.settings import Settings as RunnableHandleSettings -__all__ = ["AsyncBeekeeper", "Beekeeper", "BeekeeperNotificationHandler"] +AsyncBeekeeper = AsyncBeekeeperTemplate[RunnableHandleSettings] +Beekeeper = BeekeeperTemplate[RunnableHandleSettings] + +__all__ = [ + "AsyncBeekeeper", + "AsyncBeekeeperTemplate", + "AsyncWalletLocked", + "Beekeeper", + "BeekeeperTemplate", + "close_already_running_beekeeper", + "RunnableHandleSettings", + "SyncWalletLocked", +] diff --git a/beekeepy/beekeepy/_runnable_handle/beekeeper.py b/beekeepy/beekeepy/_runnable_handle/beekeeper.py index 58bc9f66..912ca3df 100644 --- a/beekeepy/beekeepy/_runnable_handle/beekeeper.py +++ b/beekeepy/beekeepy/_runnable_handle/beekeeper.py @@ -1,250 +1,114 @@ from __future__ import annotations -from abc import ABC, abstractmethod -from subprocess import CalledProcessError -from typing import TYPE_CHECKING, Any, TypeVar, cast - -from beekeepy._communication.universal_notification_server import ( - UniversalNotificationServer, -) -from beekeepy._executable import BeekeeperArguments, BeekeeperExecutable -from beekeepy._executable.arguments.beekeeper_arguments import ( - BeekeeperArgumentsDefaults, -) -from beekeepy._interface.url import HttpUrl -from beekeepy._remote_handle import beekeeper as remote_beekeeper -from beekeepy._runnable_handle.beekeeper_callbacks import BeekeeperNotificationCallbacks -from beekeepy._runnable_handle.beekeeper_notification_handler import NotificationHandler +from typing import TYPE_CHECKING, TypeVar + +from beekeepy._executable import BeekeeperArguments, BeekeeperConfig, BeekeeperExecutable +from beekeepy._remote_handle import AsyncBeekeeperTemplate, BeekeeperTemplate +from beekeepy._runnable_handle.runnable_handle import RunnableHandle from beekeepy._runnable_handle.settings import Settings -from beekeepy.exceptions import ( - BeekeeperFailedToStartDuringProcessSpawnError, - BeekeeperFailedToStartNotReadyOnTimeError, - BeekeeperIsNotRunningError, -) +from beekeepy.exceptions import BeekeeperFailedToStartError, ExecutableError if TYPE_CHECKING: from pathlib import Path - from loguru import Logger - - from beekeepy._executable.beekeeper_config import BeekeeperConfig - from beekeepy._interface.key_pair import KeyPair - from schemas.notifications import ( - Error, - Notification, - Status, - WebserverListening, - ) - - -EnterReturnT = TypeVar("EnterReturnT", bound=remote_beekeeper.Beekeeper | remote_beekeeper.AsyncBeekeeper) + from beekeepy._communication import HttpUrl + from beekeepy._executable import KeyPair + from beekeepy._runnable_handle.match_ports import PortMatchingResult __all__ = [ - "SyncRemoteBeekeeper", - "AsyncRemoteBeekeeper", "Beekeeper", "AsyncBeekeeper", ] +RunnableSettingsT = TypeVar("RunnableSettingsT", bound=Settings) -class SyncRemoteBeekeeper(remote_beekeeper.Beekeeper): - pass - - -class AsyncRemoteBeekeeper(remote_beekeeper.AsyncBeekeeper): - pass - - -class BeekeeperCommon(BeekeeperNotificationCallbacks, ABC): - def __init__(self, *args: Any, settings: Settings, logger: Logger, **kwargs: Any) -> None: - super().__init__(*args, settings=settings, logger=logger, **kwargs) - self.__exec = BeekeeperExecutable(settings, logger) - self.__notification_server: UniversalNotificationServer | None = None - self.__notification_event_handler: NotificationHandler | None = None - self.__logger = logger - @property - def pid(self) -> int: - if not self.is_running: - raise BeekeeperIsNotRunningError - return self.__exec.pid +class RunnableBeekeeper(RunnableHandle[BeekeeperExecutable, BeekeeperConfig, BeekeeperArguments, Settings]): + def _construct_executable(self) -> BeekeeperExecutable: + settings = self._get_settings() + return BeekeeperExecutable( + executable_path=settings.binary_path, + working_directory=settings.ensured_working_directory, + logger=self._logger, + ) - @property - def notification_endpoint(self) -> HttpUrl: - endpoint = self._get_settings().notification_endpoint - assert endpoint is not None, "Notification endpoint is not set" - return endpoint + def _get_working_directory_from_cli_arguments(self) -> Path | None: + return self.arguments.data_dir - @property - def config(self) -> BeekeeperConfig: - return self.__exec.config + def _get_http_endpoint_from_cli_arguments(self) -> HttpUrl | None: + return self.arguments.webserver_http_endpoint - @property - def is_running(self) -> bool: - return self.__exec is not None and self.__exec.is_running() + def _get_http_endpoint_from_config(self) -> HttpUrl | None: + return self.config.webserver_http_endpoint - def __setup_notification_server(self, *, address_from_cli_arguments: HttpUrl | None = None) -> None: - assert self.__notification_server is None, "Notification server already exists, previous hasn't been close?" - assert ( - self.__notification_event_handler is None - ), "Notification event handler already exists, previous hasn't been close?" + def _unify_cli_arguments(self, working_directory: Path, http_endpoint: HttpUrl) -> None: + self.arguments.data_dir = working_directory + self.arguments.webserver_http_endpoint = http_endpoint - self.__notification_event_handler = NotificationHandler(self) - self.__notification_server = UniversalNotificationServer( - self.__notification_event_handler, - notification_endpoint=address_from_cli_arguments - or self._get_settings().notification_endpoint, # this has to be accessed directly from settings - ) + def _unify_config(self, working_directory: Path, http_endpoint: HttpUrl) -> None: # noqa: ARG002 + self.config.webserver_http_endpoint = http_endpoint - def __close_notification_server(self) -> None: - if self.__notification_server is not None: - self.__notification_server.close() - self.__notification_server = None - - if self.__notification_event_handler is not None: - self.__notification_event_handler = None - - def __wait_till_ready(self) -> None: - assert self.__notification_event_handler is not None, "Notification event handler hasn't been set" - if not self.__notification_event_handler.http_listening_event.wait( - timeout=self._get_settings().initialization_timeout.total_seconds() - ): - raise TimeoutError("Waiting too long for beekeeper to be up and running") - - def _handle_error(self, error: Error) -> None: - self.__logger.error(f"Beekeepr error: `{error.json()}`") - - def _handle_status_change(self, status: Status) -> None: - self.__logger.info(f"Beekeeper status change to: `{status.current_status}`") - - def _run( - self, - settings: Settings, - additional_cli_arguments: BeekeeperArguments | None = None, - ) -> None: - aca = additional_cli_arguments or BeekeeperArguments() - self.__setup_notification_server(address_from_cli_arguments=aca.notifications_endpoint) - assert self.__notification_server is not None, "Creation of notification server failed" - settings.notification_endpoint = HttpUrl(f"127.0.0.1:{self.__notification_server.run()}", protocol="http") - settings.http_endpoint = ( - aca.webserver_http_endpoint or settings.http_endpoint or HttpUrl("127.0.0.1:0", protocol="http") - ) - settings.working_directory = ( - aca.data_dir - if aca.data_dir != BeekeeperArgumentsDefaults.DEFAULT_DATA_DIR - else self.__exec.working_directory - ) - try: - self._run_application(settings=settings, additional_cli_arguments=aca) - except CalledProcessError as e: - raise BeekeeperFailedToStartDuringProcessSpawnError from e - try: - self.__wait_till_ready() - except (AssertionError, TimeoutError) as e: - self._close() - raise BeekeeperFailedToStartNotReadyOnTimeError from e - - def _run_application(self, settings: Settings, additional_cli_arguments: BeekeeperArguments) -> None: - assert settings.notification_endpoint is not None - self.__exec.run( - blocking=False, - arguments=additional_cli_arguments.copy( - update={ - "notifications_endpoint": settings.notification_endpoint, - "webserver_http_endpoint": settings.ensured_http_endpoint, - "data_dir": settings.ensured_working_directory, - } - ), - propagate_sigint=settings.propagate_sigint, - ) + def run(self, additional_cli_arguments: BeekeeperArguments | None = None) -> None: + with self._exec.restore_arguments(additional_cli_arguments): + try: + self._run() + except ExecutableError as e: + raise BeekeeperFailedToStartError from e - def detach(self) -> int: - pid = self.__exec.detach() - self.__close_notification_server() - return pid + def _write_ports(self, editable_settings: Settings, ports: PortMatchingResult) -> None: + editable_settings.http_endpoint = ports.http + self.config.webserver_http_endpoint = ports.http + self.config.webserver_ws_endpoint = ports.websocket def _close(self) -> None: self._close_application() - self.__close_notification_server() def _close_application(self) -> None: - if self.__exec.is_running(): - self.__exec.close(self._get_settings().close_timeout.total_seconds()) - - def _http_webserver_ready(self, notification: Notification[WebserverListening]) -> None: - """It is converted by _get_http_endpoint_from_event.""" - - def _get_http_endpoint_from_event(self) -> HttpUrl: - assert self.__notification_event_handler is not None, "Notification event handler hasn't been set" - # <###> if you get exception from here, and have consistent way of reproduce please report <###> - # make sure you didn't forget to call beekeeper.run() method - addr = self.__notification_event_handler.http_endpoint_from_event - assert addr is not None, "Endpoint from event was not set" - return addr + if self._exec.is_running(): + self._exec.close(self._get_settings().close_timeout.total_seconds()) def export_keys_wallet( self, wallet_name: str, wallet_password: str, extract_to: Path | None = None ) -> list[KeyPair]: - return self.__exec.export_keys_wallet( + return self._exec.export_keys_wallet( wallet_name=wallet_name, wallet_password=wallet_password, extract_to=extract_to, ) - @abstractmethod - def _get_settings(self) -> Settings: ... - - -class Beekeeper(BeekeeperCommon, SyncRemoteBeekeeper): - def run(self, *, additional_cli_arguments: BeekeeperArguments | None = None) -> None: - self._clear_session() - with self.update_settings() as settings: - self._run( - settings=cast(Settings, settings), - additional_cli_arguments=additional_cli_arguments, - ) - self.http_endpoint = self._get_http_endpoint_from_event() +class Beekeeper(RunnableBeekeeper, BeekeeperTemplate[RunnableSettingsT]): def _get_settings(self) -> Settings: - assert isinstance(self.settings, Settings) return self.settings - @property - def settings(self) -> Settings: - return cast(Settings, super().settings) - - def _enter(self) -> Beekeeper: + def _enter(self) -> Beekeeper[RunnableSettingsT]: self.run() return self def teardown(self) -> None: self._close() + self._clear_session() super().teardown() - -class AsyncBeekeeper(BeekeeperCommon, AsyncRemoteBeekeeper): - def run(self, *, additional_cli_arguments: BeekeeperArguments | None = None) -> None: - self._clear_session() + def _setup_ports(self, ports: PortMatchingResult) -> None: with self.update_settings() as settings: - self._run( - settings=cast(Settings, settings), - additional_cli_arguments=additional_cli_arguments, - ) - self.http_endpoint = self._get_http_endpoint_from_event() + self._write_ports(settings, ports) + +class AsyncBeekeeper(RunnableBeekeeper, AsyncBeekeeperTemplate[RunnableSettingsT]): def _get_settings(self) -> Settings: - assert isinstance(self.settings, Settings) return self.settings - @property - def settings(self) -> Settings: - return cast(Settings, super().settings) - - async def _aenter(self) -> AsyncBeekeeper: + async def _aenter(self) -> AsyncBeekeeper[RunnableSettingsT]: self.run() return self def teardown(self) -> None: - self._close() + self.close() + self._clear_session() super().teardown() + + def _setup_ports(self, ports: PortMatchingResult) -> None: + with self.update_settings() as settings: + self._write_ports(settings, ports) diff --git a/beekeepy/beekeepy/_runnable_handle/beekeeper_callbacks.py b/beekeepy/beekeepy/_runnable_handle/beekeeper_callbacks.py deleted file mode 100644 index 8ca36a4c..00000000 --- a/beekeepy/beekeepy/_runnable_handle/beekeeper_callbacks.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import warnings -from abc import ABC -from typing import TYPE_CHECKING, Any, Protocol - -if TYPE_CHECKING: - from schemas.notifications import ( - AttemptClosingWallets, - Error, - Notification, - OpeningBeekeeperFailed, - Status, - WebserverListening, - ) - - -class NotificationCallback(Protocol): - def __call__(self, notification: Notification[Any]) -> None: ... - - -class BeekeeperNotificationCallbacks(ABC): # noqa: B024 - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - def _http_webserver_ready(self, notification: Notification[WebserverListening]) -> None: - self.__empty_handle_message(notification.value) - - def _handle_error(self, error: Error) -> None: - self.__empty_handle_message(error) - - def _handle_status_change(self, status: Status) -> None: - self.__empty_handle_message(status) - - def _handle_wallets_closed(self, note: AttemptClosingWallets) -> None: - self.__empty_handle_message(note) - - def _handle_opening_beekeeper_failed(self, info: OpeningBeekeeperFailed) -> None: - self.__empty_handle_message(info) - - def __empty_handle_message(self, obj: Any) -> None: - warnings.warn(f"Notification `{type(obj).__name__}` hasn't been handled", category=RuntimeWarning, stacklevel=1) diff --git a/beekeepy/beekeepy/_runnable_handle/beekeeper_notification_handler.py b/beekeepy/beekeepy/_runnable_handle/beekeeper_notification_handler.py deleted file mode 100644 index 74f91779..00000000 --- a/beekeepy/beekeepy/_runnable_handle/beekeeper_notification_handler.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import annotations - -from threading import Event -from typing import TYPE_CHECKING, Any - -from loguru import logger - -from beekeepy._interface.url import HttpUrl -from beekeepy._runnable_handle.notification_handler_base import BeekeeperNotificationHandler - -if TYPE_CHECKING: - from beekeepy._runnable_handle.beekeeper_callbacks import BeekeeperNotificationCallbacks - from schemas.notifications import ( - AttemptClosingWallets, - Error, - KnownNotificationT, - Notification, - OpeningBeekeeperFailed, - Status, - WebserverListening, - ) - - -class NotificationHandler(BeekeeperNotificationHandler): - def __init__(self, owner: BeekeeperNotificationCallbacks, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.__owner = owner - - self.http_listening_event = Event() - self.http_endpoint_from_event: HttpUrl | None = None - - self.already_working_beekeeper_event = Event() - self.already_working_beekeeper_http_address: HttpUrl | None = None - self.already_working_beekeeper_pid: int | None = None - - async def on_attempt_of_closing_wallets(self, notification: Notification[AttemptClosingWallets]) -> None: - self.__owner._handle_wallets_closed(notification.value) - - async def on_opening_beekeeper_failed(self, notification: Notification[OpeningBeekeeperFailed]) -> None: - self.already_working_beekeeper_http_address = HttpUrl( - self.__combine_url_string( - notification.value.connection.address, - notification.value.connection.port, - ), - protocol="http", - ) - self.already_working_beekeeper_pid = int(notification.value.pid) - self.already_working_beekeeper_event.set() - self.__owner._handle_opening_beekeeper_failed(notification.value) - - async def on_error(self, notification: Notification[Error]) -> None: - self.__owner._handle_error(notification.value) - - async def on_status_changed(self, notification: Notification[Status]) -> None: - self.__owner._handle_status_change(notification.value) - - async def on_http_webserver_bind(self, notification: Notification[WebserverListening]) -> None: - self.http_endpoint_from_event = HttpUrl( - self.__combine_url_string(notification.value.address, notification.value.port), - protocol="http", - ) - self.http_listening_event.set() - self.__owner._http_webserver_ready(notification) - - async def handle_notification(self, notification: Notification[KnownNotificationT]) -> None: - logger.debug(f"got notification: {notification.json()}") - return await super().handle_notification(notification) - - def __combine_url_string(self, address: str, port: int) -> str: - return f"{address}:{port}" diff --git a/beekeepy/beekeepy/_runnable_handle/match_ports.py b/beekeepy/beekeepy/_runnable_handle/match_ports.py new file mode 100644 index 00000000..09f46fb6 --- /dev/null +++ b/beekeepy/beekeepy/_runnable_handle/match_ports.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import socket +import ssl +from dataclasses import dataclass, field +from typing import Final + +from beekeepy._communication import HttpUrl, P2PUrl, WsUrl + +__all__ = ["PortMatchingResult", "match_ports"] + +# https://http.cat/status/426 +WEBSERVER_SPECIFIC_RESPONSE: Final[bytes] = b"426 Upgrade Required" + + +@dataclass +class PortMatchingResult: + http: HttpUrl | None = None + https: HttpUrl | None = None + websocket: WsUrl | None = None + p2p: list[P2PUrl] = field(default_factory=list) + + +def test_http(address: HttpUrl) -> bool: + assert address.port is not None, "HTTP CHECK: Port has to be set" + try: + with socket.create_connection((address.address, address.port), timeout=1) as sock: + sock.sendall(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + response = sock.recv(1024) + return response.startswith(b"HTTP") and WEBSERVER_SPECIFIC_RESPONSE not in response + except (OSError, socket.timeout, ConnectionRefusedError): + return False + + +def test_https(address: HttpUrl) -> bool: + assert address.port is not None, "HTTPS CHECK: Port has to be set" + try: + context = ssl.create_default_context() + with socket.create_connection((address.address, address.port), timeout=1) as sock, context.wrap_socket( + sock, server_hostname="localhost" + ) as ssock: + ssock.sendall(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n") + response = ssock.recv(1024) + return response.startswith(b"HTTP") and WEBSERVER_SPECIFIC_RESPONSE not in response + except (OSError, socket.timeout, ConnectionRefusedError, ssl.SSLError): + return False + + +def test_websocket(address: WsUrl) -> bool: + assert address.port is not None, "WS CHECK: Port has to be set" + try: + with socket.create_connection((address.address, address.port), timeout=1) as sock: + sock.sendall(b"GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n") + response = sock.recv(1024) + return WEBSERVER_SPECIFIC_RESPONSE in response + except (OSError, socket.timeout, ConnectionRefusedError): + return False + + +def match_ports(ports: list[int], *, address: str = "127.0.0.1") -> PortMatchingResult: + categories = PortMatchingResult() + for port in ports: + if categories.http is None and test_http(http_result := HttpUrl.factory(port=port, address=address)): + categories.http = http_result + elif categories.https is None and test_https(http_result := HttpUrl.factory(port=port, address=address)): + categories.https = http_result + elif categories.websocket is None and test_websocket(ws_result := WsUrl.factory(port=port, address=address)): + categories.websocket = ws_result + else: + categories.p2p.append(P2PUrl.factory(port=port, address=address)) + + return categories diff --git a/beekeepy/beekeepy/_runnable_handle/notification_handler_base.py b/beekeepy/beekeepy/_runnable_handle/notification_handler_base.py deleted file mode 100644 index 1e3195be..00000000 --- a/beekeepy/beekeepy/_runnable_handle/notification_handler_base.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from beekeepy._communication.appbase_notification_handler import AppbaseNotificationHandler -from beekeepy._communication.notification_decorator import notification -from schemas.notifications import AttemptClosingWallets, OpeningBeekeeperFailed - -if TYPE_CHECKING: - from schemas.notifications import Notification - - -class BeekeeperNotificationHandler(AppbaseNotificationHandler): - @notification(AttemptClosingWallets) - async def _on_attempt_of_closing_wallets(self, notification: Notification[AttemptClosingWallets]) -> None: - await self.on_attempt_of_closing_wallets(notification) - - @notification(OpeningBeekeeperFailed) - async def _on_opening_beekeeper_failed(self, notification: Notification[OpeningBeekeeperFailed]) -> None: - await self.on_opening_beekeeper_failed(notification) - - async def on_attempt_of_closing_wallets(self, notification: Notification[AttemptClosingWallets]) -> None: - """Called when beekeeper attempts to close wallets in session with given token.""" - - async def on_opening_beekeeper_failed(self, notification: Notification[OpeningBeekeeperFailed]) -> None: - """Called, when beekeeper failed to start. - - That is because of already running other beekeeper in selected working directory. - """ diff --git a/beekeepy/beekeepy/_runnable_handle/runnable_handle.py b/beekeepy/beekeepy/_runnable_handle/runnable_handle.py new file mode 100644 index 00000000..5efd171f --- /dev/null +++ b/beekeepy/beekeepy/_runnable_handle/runnable_handle.py @@ -0,0 +1,256 @@ +from __future__ import annotations + +import time +import warnings +from abc import ABC, abstractmethod +from datetime import timedelta +from pathlib import Path +from subprocess import SubprocessError +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast + +from loguru import logger as default_logger + +from beekeepy._communication import HttpUrl, P2PUrl, WsUrl +from beekeepy._executable import ArgumentT, ConfigT, Executable +from beekeepy._remote_handle import AppStatusProbe +from beekeepy._runnable_handle.match_ports import PortMatchingResult, match_ports +from beekeepy._runnable_handle.settings import Settings +from beekeepy.exceptions import ( + ApiNotFoundError, + FailedToDetectReservedPortsError, + FailedToStartExecutableError, +) + +if TYPE_CHECKING: + from loguru import Logger + + from schemas.apis.app_status_api import GetAppStatus + + +ExecutableT = TypeVar("ExecutableT", bound=Executable[Any, Any]) +SettingsT = TypeVar("SettingsT", bound=Settings) +T = TypeVar("T") + + +class RunnableHandle(ABC, Generic[ExecutableT, ConfigT, ArgumentT, SettingsT]): + def __init__(self, *args: Any, logger: Logger | None = None, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._logger = logger or default_logger + self._exec = self._construct_executable() + + @property + def pid(self) -> int: + """Returns pid of started executable. Note: Proxy method to Executable.pid.""" + return self._exec.pid + + @property + def arguments(self) -> ArgumentT: + """Returns arguments for given binary. Note: Proxy method to Executable.arguments.""" + return cast(ArgumentT, self._exec.arguments) + + @property + def config(self) -> ConfigT: + """Returns config for given binary. Note: Proxy method to Executable.config.""" + return cast(ConfigT, self._exec.config) + + def is_running(self) -> bool: + """Returns is process running. Note: Proxy method to Executable.is_running.""" + return self._exec.is_running() + + def detach(self) -> int: + """Detaches process and allows to keep it after closing python script.""" + return self._exec.detach() + + def close(self) -> None: + """Closes running process. If process is not running, method does nothing.""" + if self.is_running(): + self._exec.close(timeout_secs=self._get_settings().close_timeout.total_seconds()) + + def get_help_text(self) -> str: + """Returns help printed by executable.""" + self.__show_warning_if_executable_already_running() + return self._exec.get_help_text() + + def get_version(self) -> str: + """Returns version string printed by executable.""" + self.__show_warning_if_executable_already_running() + return self._exec.version() + + def generate_default_config_from_executable(self) -> ConfigT: + """Returns config generated by executable.""" + self.__show_warning_if_executable_already_running() + return cast(ConfigT, self._exec.generate_default_config()) + + def _run( + self, + *, + environment_variables: dict[str, str] | None = None, + perform_unification: bool = True, + blocking: bool = False, + save_config: bool = True, + ) -> None: + """ + Runs executable and unifies arguments. + + Note: This method should be called by RunnableHandleChild.run, which is not defined by this interface! + + Keyword Arguments: + environment_variables -- additional environment variables to set before launching executable + additional_cli_arguments -- arguments to add to executable invocation + perform_unification -- if set to true, chosen values will be written to config and cli arguments + """ + settings = self._get_settings().copy() + + settings.working_directory = self.__choose_working_directory(settings=settings) + settings.http_endpoint = self.__choose_http_endpoint(settings=settings) + + if perform_unification: + self._unify_cli_arguments(settings.working_directory, settings.http_endpoint) + self._unify_config(settings.working_directory, settings.http_endpoint) + + try: + self._exec._run( + blocking=blocking, + environ=environment_variables, + propagate_sigint=settings.propagate_sigint, + save_config=save_config, + ) + if blocking: + return + except SubprocessError as e: + raise FailedToStartExecutableError from e + try: + self._wait_for_app_to_start() + except TimeoutError as e: + raise FailedToDetectReservedPortsError from e + self._setup_ports(self.__discover_ports()) + + @abstractmethod + def _construct_executable(self) -> ExecutableT: + """Returns executable instance.""" + + @abstractmethod + def _get_settings(self) -> SettingsT: + """Returns settings hold by child class. Used only for read-only purposes.""" + + def _get_working_directory_from_cli_arguments(self) -> Path | None: + """Returns working directory from specified cli arguments in executable (if specified).""" + return None + + def _get_http_endpoint_from_cli_arguments(self) -> HttpUrl | None: + """Returns http endpoint from specified cli arguments in executable (if specified).""" + return None + + def _get_working_directory_from_config(self) -> Path | None: + """Returns working directory from specified config in executable (if specified).""" + return None + + def _get_http_endpoint_from_config(self) -> HttpUrl | None: + """Returns http endpoint from specified config in executable (if specified).""" + return None + + @abstractmethod + def _unify_cli_arguments(self, working_directory: Path, http_endpoint: HttpUrl) -> None: + """ + Writes selected values to given cli arguments. + + Args: + working_directory -- chosen working path to be set in cli arguments. + http_endpoint -- chosen http endpoint to be set in cli arguments. + """ + + @abstractmethod + def _unify_config(self, working_directory: Path, http_endpoint: HttpUrl) -> None: + """ + Writes selected values to config in executable. + + Args: + working_directory -- chosen working path to be set in config. + http_endpoint -- chosen http endpoint to be set in config. + """ + + def _setup_ports(self, ports: PortMatchingResult) -> None: + """ + Setup ports after startup. + + Args: + ports -- list of ports reserved by started application. + """ + + def _wait_for_app_to_start(self) -> None: + """Waits for application to start.""" + while not self._exec.reserved_ports(): + if not self._exec.is_running(): + raise FailedToStartExecutableError + time.sleep(0.1) + + def __choose_working_directory(self, settings: Settings) -> Path: + return self.__choose_value( + default_value=Path.cwd(), + argument_value=self._get_working_directory_from_cli_arguments(), + config_value=self._get_working_directory_from_config(), + settings_value=settings.working_directory, + ) + + def __choose_http_endpoint(self, settings: Settings) -> HttpUrl: + return self.__choose_value( + default_value=HttpUrl("http://0.0.0.0:0"), + argument_value=self._get_http_endpoint_from_cli_arguments(), + config_value=self._get_http_endpoint_from_config(), + settings_value=settings.http_endpoint, + ) + + def __show_warning_if_executable_already_running(self) -> None: + if self.is_running(): + warnings.warn("Invoking executable that is already running!", stacklevel=2) + + @classmethod + def __choose_value( + cls, + default_value: T, + argument_value: T | None = None, + config_value: T | None = None, + settings_value: T | None = None, + ) -> T: + if argument_value is not None: + return argument_value + if config_value is not None: + return config_value + if settings_value is not None: + return settings_value + return default_value + + def __discover_ports(self) -> PortMatchingResult: + reserved_ports = self._exec.reserved_ports() + matched_ports = match_ports(reserved_ports) + if matched_ports.http is None: + warnings.warn("Given executable probably does not provide http network access", stacklevel=3) + return matched_ports + + handle = AppStatusProbe(settings=Settings(http_endpoint=matched_ports.http, timeout=timedelta(seconds=1))) + status: None | GetAppStatus = None + try: + status = handle.api.get_app_status() + except ApiNotFoundError: + warnings.warn( + "HTTP port detected, but cannot obtain further information. app_status_api plugin is not enabled!", + stacklevel=3, + ) + return matched_ports + + assert status is not None, "Error has not been caught and further port discovery started" + http = status.webservers.HTTP + assert http, "Http cannot be None, as AppStatusProbe is already connected via http" + assert ( + http.port == matched_ports.http.port + ), "Http cannot differ from detected ports, because it is already connected" + + ws = status.webservers.WS + if ws and matched_ports.websocket and ws.port != matched_ports.websocket.port: + matched_ports.websocket = WsUrl.factory(port=ws.port) + + p2p = status.webservers.P2P + if p2p and p2p.port not in [x.port for x in matched_ports.p2p]: + matched_ports.p2p = [P2PUrl.factory(port=p2p.port)] + + return matched_ports diff --git a/beekeepy/beekeepy/_runnable_handle/settings.py b/beekeepy/beekeepy/_runnable_handle/settings.py index c989f3e4..1ee8b0c2 100644 --- a/beekeepy/beekeepy/_runnable_handle/settings.py +++ b/beekeepy/beekeepy/_runnable_handle/settings.py @@ -5,13 +5,12 @@ from distutils.util import strtobool from pathlib import Path from typing import ClassVar -from beekeepy._communication.abc.communicator import AbstractCommunicator # noqa: TCH001 -from beekeepy._interface.url import HttpUrl # noqa: TCH001 -from beekeepy._remote_handle.settings import Settings as RemoteHandleSettings +from beekeepy._communication import HttpUrl # noqa: TCH001 +from beekeepy._remote_handle import RemoteHandleSettings class Settings(RemoteHandleSettings): - """Defines parameters for beekeeper how to start and behave.""" + """Defines parameters for runnable handles how to start and behave.""" class EnvironNames(RemoteHandleSettings.EnvironNames): WORKING_DIRECTORY: ClassVar[str] = "BEEKEEPY_WORKING_DIRECTORY" @@ -34,16 +33,6 @@ class Settings(RemoteHandleSettings): In case of local beekeeper, this address will be used for beekeeper to start listening on. """ - communicator: type[AbstractCommunicator] | AbstractCommunicator | None = None - """ - Defines class to be used for network handling. Can be given as class or instance. - - Note: If set to none, handles will use preferred communicators - """ - - notification_endpoint: HttpUrl | None = None - """Endpoint to use for reverse communication between beekeeper and python.""" - binary_path: Path | None = None """Alternative path to beekeeper binary.""" @@ -66,6 +55,7 @@ class Settings(RemoteHandleSettings): EnvironNames.INITIALIZATION_TIMEOUT, lambda x: (Settings.Defaults.INITIALIZATION_TIMEOUT if x is None else timedelta(seconds=int(x))), ) + """Affects time handle waits for beekeeper to start.""" @property def ensured_working_directory(self) -> Path: diff --git a/beekeepy/beekeepy/_executable/arguments/__init__.py b/beekeepy/beekeepy/_utilities/__init__.py similarity index 100% rename from beekeepy/beekeepy/_executable/arguments/__init__.py rename to beekeepy/beekeepy/_utilities/__init__.py diff --git a/beekeepy/beekeepy/_remote_handle/build_json_rpc_call.py b/beekeepy/beekeepy/_utilities/build_json_rpc_call.py similarity index 100% rename from beekeepy/beekeepy/_remote_handle/build_json_rpc_call.py rename to beekeepy/beekeepy/_utilities/build_json_rpc_call.py diff --git a/beekeepy/beekeepy/_interface/context.py b/beekeepy/beekeepy/_utilities/context.py similarity index 100% rename from beekeepy/beekeepy/_interface/context.py rename to beekeepy/beekeepy/_utilities/context.py diff --git a/beekeepy/beekeepy/_interface/context_settings_updater.py b/beekeepy/beekeepy/_utilities/context_settings_updater.py similarity index 93% rename from beekeepy/beekeepy/_interface/context_settings_updater.py rename to beekeepy/beekeepy/_utilities/context_settings_updater.py index a10bbc93..ca41424a 100644 --- a/beekeepy/beekeepy/_interface/context_settings_updater.py +++ b/beekeepy/beekeepy/_utilities/context_settings_updater.py @@ -4,12 +4,12 @@ from abc import abstractmethod from contextlib import contextmanager from typing import TYPE_CHECKING, Generic, TypeVar -from beekeepy._communication.settings import CommunicationSettings +from pydantic import BaseModel if TYPE_CHECKING: from collections.abc import Iterator -SettingsT = TypeVar("SettingsT", bound=CommunicationSettings) +SettingsT = TypeVar("SettingsT", bound=BaseModel) class ContextSettingsUpdater(Generic[SettingsT]): diff --git a/beekeepy/beekeepy/_interface/delay_guard.py b/beekeepy/beekeepy/_utilities/delay_guard.py similarity index 97% rename from beekeepy/beekeepy/_interface/delay_guard.py rename to beekeepy/beekeepy/_utilities/delay_guard.py index 670bcc46..918ed820 100644 --- a/beekeepy/beekeepy/_interface/delay_guard.py +++ b/beekeepy/beekeepy/_utilities/delay_guard.py @@ -5,7 +5,7 @@ import time from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Final -from beekeepy._interface.context import ContextAsync, ContextSync +from beekeepy._utilities.context import ContextAsync, ContextSync from beekeepy.exceptions import UnlockIsNotAccessibleError if TYPE_CHECKING: diff --git a/beekeepy/beekeepy/_interface/error_logger.py b/beekeepy/beekeepy/_utilities/error_logger.py similarity index 93% rename from beekeepy/beekeepy/_interface/error_logger.py rename to beekeepy/beekeepy/_utilities/error_logger.py index 76037104..dd2872cc 100644 --- a/beekeepy/beekeepy/_interface/error_logger.py +++ b/beekeepy/beekeepy/_utilities/error_logger.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from loguru import logger as loguru_logger -from beekeepy._interface.context import ContextSync +from beekeepy._utilities.context import ContextSync if TYPE_CHECKING: from types import TracebackType @@ -13,7 +13,7 @@ if TYPE_CHECKING: class ErrorLogger(ContextSync[None]): - def __init__(self, logger: Logger | None = None, *exceptions: type[Exception]) -> None: + def __init__(self, logger: Logger | None = None, *exceptions: type[BaseException]) -> None: super().__init__() self.__logger = logger or loguru_logger self.__exception_whitelist = list(exceptions) diff --git a/beekeepy/beekeepy/_interface/key_pair.py b/beekeepy/beekeepy/_utilities/key_pair.py similarity index 100% rename from beekeepy/beekeepy/_interface/key_pair.py rename to beekeepy/beekeepy/_utilities/key_pair.py diff --git a/beekeepy/beekeepy/_interface/_sanitize.py b/beekeepy/beekeepy/_utilities/sanitize.py similarity index 100% rename from beekeepy/beekeepy/_interface/_sanitize.py rename to beekeepy/beekeepy/_utilities/sanitize.py diff --git a/beekeepy/beekeepy/_interface/settings_holder.py b/beekeepy/beekeepy/_utilities/settings_holder.py similarity index 97% rename from beekeepy/beekeepy/_interface/settings_holder.py rename to beekeepy/beekeepy/_utilities/settings_holder.py index ae98400d..e4a3ccae 100644 --- a/beekeepy/beekeepy/_interface/settings_holder.py +++ b/beekeepy/beekeepy/_utilities/settings_holder.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from contextlib import contextmanager from typing import TYPE_CHECKING, Any -from beekeepy._interface.context_settings_updater import ContextSettingsUpdater, SettingsT +from beekeepy._utilities.context_settings_updater import ContextSettingsUpdater, SettingsT if TYPE_CHECKING: from collections.abc import Iterator diff --git a/beekeepy/beekeepy/_interface/state_invalidator.py b/beekeepy/beekeepy/_utilities/state_invalidator.py similarity index 100% rename from beekeepy/beekeepy/_interface/state_invalidator.py rename to beekeepy/beekeepy/_utilities/state_invalidator.py diff --git a/beekeepy/beekeepy/_interface/stopwatch.py b/beekeepy/beekeepy/_utilities/stopwatch.py similarity index 95% rename from beekeepy/beekeepy/_interface/stopwatch.py rename to beekeepy/beekeepy/_utilities/stopwatch.py index 094f09a4..61b751fe 100644 --- a/beekeepy/beekeepy/_interface/stopwatch.py +++ b/beekeepy/beekeepy/_utilities/stopwatch.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta, timezone from typing import Any -from beekeepy._interface.context import ContextSync +from beekeepy._utilities.context import ContextSync @dataclass diff --git a/beekeepy/beekeepy/_interface/_suppress_api_not_found.py b/beekeepy/beekeepy/_utilities/suppress_api_not_found.py similarity index 96% rename from beekeepy/beekeepy/_interface/_suppress_api_not_found.py rename to beekeepy/beekeepy/_utilities/suppress_api_not_found.py index 4b7758cd..2fb37cbf 100644 --- a/beekeepy/beekeepy/_interface/_suppress_api_not_found.py +++ b/beekeepy/beekeepy/_utilities/suppress_api_not_found.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from beekeepy._interface.context import SelfContextSync +from beekeepy._utilities.context import SelfContextSync from beekeepy.exceptions import ApiNotFoundError, GroupedErrorsError if TYPE_CHECKING: diff --git a/beekeepy/beekeepy/exceptions/__init__.py b/beekeepy/beekeepy/exceptions/__init__.py index 6faeb756..cfcf6832 100644 --- a/beekeepy/beekeepy/exceptions/__init__.py +++ b/beekeepy/beekeepy/exceptions/__init__.py @@ -7,8 +7,8 @@ from beekeepy.exceptions.base import ( BeekeeperInterfaceError, BeekeepyError, CommunicationError, - CommunicationResponseT, DetectableError, + ExecutableError, InvalidatedStateError, Json, OverseerError, @@ -46,6 +46,14 @@ from beekeepy.exceptions.detectable import ( NoWalletWithSuchNameError, WalletWithSuchNameAlreadyExistsError, ) +from beekeepy.exceptions.executable import ( + ExecutableIsNotRunningError, + FailedToDetectReservedPortsError, + FailedToStartExecutableError, +) +from beekeepy.exceptions.executable import ( + TimeoutReachWhileCloseError as ExecutableTimeoutReachWhileCloseError, +) from beekeepy.exceptions.overseer import ( ApiNotFoundError, DifferenceBetweenAmountOfRequestsAndResponsesError, @@ -74,11 +82,15 @@ __all__ = [ "BeekeeperIsNotRunningError", "BeekeepyError", "CommunicationError", - "CommunicationResponseT", "DetachRemoteBeekeeperError", "DetectableError", "DifferenceBetweenAmountOfRequestsAndResponsesError", "ErrorInResponseError", + "ExecutableError", + "ExecutableIsNotRunningError", + "ExecutableTimeoutReachWhileCloseError", + "FailedToDetectReservedPortsError", + "FailedToStartExecutableError", "GroupedErrorsError", "InvalidAccountNameError", "InvalidatedStateByClosingBeekeeperError", @@ -100,8 +112,8 @@ __all__ = [ "NotPositiveTimeError", "NoWalletWithSuchNameError", "NullResultError", - "OverseerInvalidPasswordError", "OverseerError", + "OverseerInvalidPasswordError", "ResponseNotReadyError", "SchemaDetectableError", "TimeoutExceededError", diff --git a/beekeepy/beekeepy/exceptions/base.py b/beekeepy/beekeepy/exceptions/base.py index 61c77cc9..ba088f05 100644 --- a/beekeepy/beekeepy/exceptions/base.py +++ b/beekeepy/beekeepy/exceptions/base.py @@ -5,18 +5,18 @@ from typing import TYPE_CHECKING, Any from pydantic import StrRegexError -from beekeepy._interface.context import ContextSync +from beekeepy._utilities.context import ContextSync if TYPE_CHECKING: from types import TracebackType - from beekeepy._interface.url import Url + from beekeepy._communication import Url Json = dict[str, Any] CommunicationResponseT = str | Json | list[Json] -class BeekeepyError(Exception): +class BeekeepyError(BaseException, ABC): """Base class for all exception raised by beekeepy.""" @property @@ -189,3 +189,7 @@ class OverseerError(CommunicationError, ABC): @abstractmethod def retry(self) -> bool: """Used by overseer to determine if retry should be performed if such error occurs.""" + + +class ExecutableError(BeekeepyError, ABC): + """Base class for errors related to handling executable.""" diff --git a/beekeepy/beekeepy/exceptions/common.py b/beekeepy/beekeepy/exceptions/common.py index 29b3efd0..eccdb9ab 100644 --- a/beekeepy/beekeepy/exceptions/common.py +++ b/beekeepy/beekeepy/exceptions/common.py @@ -13,7 +13,7 @@ from beekeepy.exceptions.base import ( ) if TYPE_CHECKING: - from beekeepy._interface.url import Url + from beekeepy._communication import Url class BatchRequestError(BeekeepyError): @@ -93,6 +93,14 @@ class TimeTooBigError(BeekeepyError): super().__init__(f"Given time value is too big: `{time}` >= {TimeTooBigError.MAX_VALUE}.") +class InvalidOptionError(BeekeepyError): + """Raised if invalid expression is given in config.""" + + +class UnknownDecisionPathError(BeekeepyError): + """Error created to suppress mypy error: `Missing return statement [return]`.""" + + class DetachRemoteBeekeeperError(BeekeeperHandleError): """Raises when user tries to detach beekeeper that is remote.""" @@ -128,14 +136,6 @@ class InvalidatedStateByClosingSessionError(InvalidatedStateError): ) -class InvalidOptionError(BeekeepyError): - """Raised if invalid expression is given in config.""" - - -class UnknownDecisionPathError(BeekeepyError): - """Error created to suppress mypy error: `Missing return statement [return]`.""" - - class TimeoutExceededError(CommunicationError): """Raised if exceeded time for response.""" diff --git a/beekeepy/beekeepy/exceptions/executable.py b/beekeepy/beekeepy/exceptions/executable.py new file mode 100644 index 00000000..fafda8de --- /dev/null +++ b/beekeepy/beekeepy/exceptions/executable.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from beekeepy.exceptions.base import ExecutableError + + +class TimeoutReachWhileCloseError(ExecutableError): + """Raises when executable did not closed during specified timeout.""" + + def __init__(self) -> None: + """Constructor.""" + super().__init__("Process was force-closed with SIGKILL, because didn't close before timeout") + + +class ExecutableIsNotRunningError(ExecutableError): + """Raises when executable is not running, but user requests action on running instance.""" + + +class FailedToStartExecutableError(ExecutableError): + """Raises when executable failed to start.""" + + +class FailedToDetectReservedPortsError(ExecutableError): + """Raises when port lookup procedure fails.""" diff --git a/beekeepy/beekeepy/exceptions/overseer.py b/beekeepy/beekeepy/exceptions/overseer.py index 1fdc616d..4007e825 100644 --- a/beekeepy/beekeepy/exceptions/overseer.py +++ b/beekeepy/beekeepy/exceptions/overseer.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Sequence from beekeepy.exceptions.base import BeekeepyError, CommunicationResponseT, Json, OverseerError if TYPE_CHECKING: - from beekeepy._interface.url import Url + from beekeepy._communication import Url class UnableToAcquireDatabaseLockError(OverseerError): @@ -110,7 +110,7 @@ class ErrorInResponseError(OverseerError): class GroupedErrorsError(BeekeepyError): - def __init__(self, exceptions: Sequence[Exception]) -> None: + def __init__(self, exceptions: Sequence[BaseException]) -> None: self.exceptions = list(exceptions) def get_exception_for(self, *, request_id: int) -> OverseerError | None: diff --git a/beekeepy/beekeepy/handle/remote.py b/beekeepy/beekeepy/handle/remote.py index e0a6bea4..d79c765f 100644 --- a/beekeepy/beekeepy/handle/remote.py +++ b/beekeepy/beekeepy/handle/remote.py @@ -1,11 +1,21 @@ from __future__ import annotations -from beekeepy._remote_handle.abc.api import AbstractAsyncApi, AbstractSyncApi, AsyncHandleT -from beekeepy._remote_handle.abc.api_collection import AbstractAsyncApiCollection, AbstractSyncApiCollection -from beekeepy._remote_handle.abc.handle import AbstractAsyncHandle, AbstractSyncHandle -from beekeepy._remote_handle.batch_handle import AsyncBatchHandle, SyncBatchHandle -from beekeepy._remote_handle.beekeeper import AsyncBeekeeper, Beekeeper -from beekeepy._remote_handle.settings import Settings as RemoteSettings +from beekeepy._apis.abc import ( + AbstractAsyncApi, + AbstractAsyncApiCollection, + AbstractSyncApi, + AbstractSyncApiCollection, + RegisteredApisT, +) +from beekeepy._remote_handle import ( + AbstractAsyncHandle, + AbstractSyncHandle, + AsyncBatchHandle, + AsyncBeekeeper, + Beekeeper, + SyncBatchHandle, +) +from beekeepy._remote_handle import RemoteHandleSettings as RemoteSettings __all__ = [ "AbstractAsyncApi", @@ -16,8 +26,8 @@ __all__ = [ "AbstractSyncHandle", "AsyncBatchHandle", "AsyncBeekeeper", - "AsyncHandleT", "Beekeeper", + "RegisteredApisT", "RemoteSettings", "SyncBatchHandle", ] diff --git a/beekeepy/beekeepy/handle/runnable.py b/beekeepy/beekeepy/handle/runnable.py index 0b2b567c..247ed9e6 100644 --- a/beekeepy/beekeepy/handle/runnable.py +++ b/beekeepy/beekeepy/handle/runnable.py @@ -1,27 +1,14 @@ from __future__ import annotations -from beekeepy._communication.appbase_notification_handler import AppbaseNotificationHandler -from beekeepy._communication.async_server import AsyncHttpServer -from beekeepy._communication.notification_decorator import notification -from beekeepy._communication.universal_notification_server import UniversalNotificationHandler -from beekeepy._executable.arguments.beekeeper_arguments import BeekeeperArguments, BeekeeperArgumentsDefaults -from beekeepy._executable.beekeeper_config import BeekeeperConfig -from beekeepy._runnable_handle.beekeeper import AsyncBeekeeper, Beekeeper -from beekeepy._runnable_handle.close_already_running_beekeeper import close_already_running_beekeeper -from beekeepy._runnable_handle.notification_handler_base import BeekeeperNotificationHandler -from beekeepy._runnable_handle.settings import Settings as RunnableSettings +from beekeepy._executable import BeekeeperArguments, BeekeeperConfig, BeekeeperExecutable +from beekeepy._runnable_handle import AsyncBeekeeper, Beekeeper, RunnableHandleSettings, close_already_running_beekeeper __all__ = [ - "AppbaseNotificationHandler", "AsyncBeekeeper", - "AsyncHttpServer", "Beekeeper", - "BeekeeperArguments", - "BeekeeperArgumentsDefaults", "BeekeeperConfig", - "BeekeeperNotificationHandler", + "BeekeeperArguments", + "BeekeeperExecutable", "close_already_running_beekeeper", - "notification", - "RunnableSettings", - "UniversalNotificationHandler", + "RunnableHandleSettings", ] diff --git a/beekeepy/beekeepy/interfaces.py b/beekeepy/beekeepy/interfaces.py index 65742fb8..0cc6f4b8 100644 --- a/beekeepy/beekeepy/interfaces.py +++ b/beekeepy/beekeepy/interfaces.py @@ -1,22 +1,14 @@ from __future__ import annotations -from beekeepy._interface._sanitize import mask, sanitize -from beekeepy._interface._suppress_api_not_found import SuppressApiNotFound -from beekeepy._interface.context import ( - ContextAsync, - ContextSync, - SelfContextAsync, - SelfContextSync, -) -from beekeepy._interface.context_settings_updater import ContextSettingsUpdater -from beekeepy._interface.error_logger import ErrorLogger -from beekeepy._interface.key_pair import KeyPair -from beekeepy._interface.settings_holder import ( - SharedSettingsHolder, - UniqueSettingsHolder, -) -from beekeepy._interface.stopwatch import Stopwatch, StopwatchResult -from beekeepy._interface.url import HttpUrl, P2PUrl, Url, WsUrl +from beekeepy._communication import HttpUrl, P2PUrl, Url, WsUrl +from beekeepy._utilities.context import ContextAsync, ContextSync, SelfContextAsync, SelfContextSync +from beekeepy._utilities.context_settings_updater import ContextSettingsUpdater +from beekeepy._utilities.error_logger import ErrorLogger +from beekeepy._utilities.key_pair import KeyPair +from beekeepy._utilities.sanitize import mask, sanitize +from beekeepy._utilities.settings_holder import SharedSettingsHolder, UniqueSettingsHolder +from beekeepy._utilities.stopwatch import Stopwatch, StopwatchResult +from beekeepy._utilities.suppress_api_not_found import SuppressApiNotFound __all__ = [ "ContextAsync", diff --git a/beekeepy/poetry.lock b/beekeepy/poetry.lock index 03dc514c..b5137a66 100644 --- a/beekeepy/poetry.lock +++ b/beekeepy/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -633,6 +633,35 @@ files = [ {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, ] +[[package]] +name = "psutil" +version = "6.0.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, + {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, + {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, + {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + [[package]] name = "pydantic" version = "1.10.18" @@ -746,12 +775,12 @@ idna2008 = ["idna"] [[package]] name = "schemas" -version = "0.0.1.dev323+e5a1ba1" +version = "0.0.1.dev333+540050d" description = "Tools for checking if message fits expected format" optional = false python-versions = ">=3.10,<4.0" files = [ - {file = "schemas-0.0.1.dev323+e5a1ba1-py3-none-any.whl", hash = "sha256:b97546f24f54f58d71fade176e3fdfa8e113c040d35f56f04e5a567b0fa63d5f"}, + {file = "schemas-0.0.1.dev333+540050d-py3-none-any.whl", hash = "sha256:0bdaab260640262a8b4b64e1a4c9fb7aefcedc0f418eb1a646fbad491a934ac7"}, ] [package.dependencies] @@ -924,4 +953,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "a8172408e6af9484194ca2369b1bbd3049cfc50956de26b95b59f9adf47cd805" +content-hash = "8b8fb493bd5f2936397077133f358724fda104dc89766ace31c422b805f384a4" diff --git a/beekeepy/pyproject.toml b/beekeepy/pyproject.toml index 48aae02f..dd6998bb 100644 --- a/beekeepy/pyproject.toml +++ b/beekeepy/pyproject.toml @@ -23,14 +23,14 @@ source = [ version = "0.0.0" [tool.poetry.dependencies] +python = "^3.10" aiohttp = "3.9.1" httpx = {extras = ["http2"], version = "0.23.3"} loguru = "0.7.2" -python = "^3.10" python-dateutil = "2.8.2" -pydantic="1.10.18" +psutil = "6.0.0" requests = "2.27.1" -schemas = "0.0.1.dev323+e5a1ba1" +schemas = "0.0.1.dev333+540050d" [tool.poetry-dynamic-versioning] enable = true diff --git a/hive b/hive index ed702f86..40e6bc38 160000 --- a/hive +++ b/hive @@ -1 +1 @@ -Subproject commit ed702f86d3a1ae567b2d2e1d0d7240a187223964 +Subproject commit 40e6bc384b63fe116f370153a20c15a46888011f diff --git a/tests/beekeepy_test/handle/api_tests/test_api_close.py b/tests/beekeepy_test/handle/api_tests/test_api_close.py index 53999acf..dc521f72 100644 --- a/tests/beekeepy_test/handle/api_tests/test_api_close.py +++ b/tests/beekeepy_test/handle/api_tests/test_api_close.py @@ -14,6 +14,10 @@ if TYPE_CHECKING: from beekeepy.handle.runnable import Beekeeper +# NOTE 1: Beekeeper should not raise exception while calling close on already closed wallet or not existing wallet. +# It should be treated as a success for UX purposes. + + def test_api_close(beekeeper: Beekeeper, wallet: WalletInfo) -> None: """Test test_api_close will test beekeeper_api.close api call.""" # ARRANGE @@ -51,8 +55,8 @@ def test_api_close_double_close( beekeeper.api.close(wallet_name=wallet.name) # ASSERT - with pytest.raises(ErrorInResponseError, match=f"Wallet not found: {wallet.name}"): - beekeeper.api.close(wallet_name=wallet.name) + # SHOULD NOT RAISE (NOTE 1) + beekeeper.api.close(wallet_name=wallet.name) def test_api_close_not_existing_wallet(beekeeper: Beekeeper) -> None: @@ -61,5 +65,5 @@ def test_api_close_not_existing_wallet(beekeeper: Beekeeper) -> None: wallet = WalletInfo(password=generate_wallet_password(), name=generate_wallet_name()) # ACT & ASSERT - with pytest.raises(ErrorInResponseError, match=f"Wallet not found: {wallet.name}"): - beekeeper.api.close(wallet_name=wallet.name) + # SHOULD NOT RAISE (NOTE 1) + beekeeper.api.close(wallet_name=wallet.name) diff --git a/tests/beekeepy_test/handle/api_tests/test_api_close_session.py b/tests/beekeepy_test/handle/api_tests/test_api_close_session.py index 81168d1f..a3a99f52 100644 --- a/tests/beekeepy_test/handle/api_tests/test_api_close_session.py +++ b/tests/beekeepy_test/handle/api_tests/test_api_close_session.py @@ -55,11 +55,7 @@ def test_api_close_session_not_existing(create_session: bool, beekeeper: Beekeep """Test test_api_close_session_not_existing will test possibility of closing not existing session.""" # ARRANGE if create_session: - assert beekeeper.settings.notification_endpoint is not None - beekeeper.api.create_session( - notifications_endpoint=beekeeper.settings.notification_endpoint.as_string(with_protocol=False), - salt="salt", - ) + beekeeper.api.create_session(salt="salt") # ACT & ASSERT beekeeper.set_session_token(WRONG_TOKEN) diff --git a/tests/beekeepy_test/handle/api_tests/test_api_create_session.py b/tests/beekeepy_test/handle/api_tests/test_api_create_session.py index 7201ab68..b206c0d0 100644 --- a/tests/beekeepy_test/handle/api_tests/test_api_create_session.py +++ b/tests/beekeepy_test/handle/api_tests/test_api_create_session.py @@ -14,17 +14,11 @@ if TYPE_CHECKING: def create_session(beekeeper: Beekeeper, salt: str) -> None: # ARRANGE - assert beekeeper.settings.notification_endpoint is not None - notification_endpoint = beekeeper.settings.notification_endpoint.as_string(with_protocol=False) - message_to_check = ( - '"id":0,"jsonrpc":"2.0","method":"beekeeper_api.create_session",' - f'"params":{{"notifications_endpoint":"{notification_endpoint}","salt":"{salt}"}}' - ) + message_to_check = '"id":0,"jsonrpc":"2.0","method":"beekeeper_api.create_session",' f'"params":{{"salt":"{salt}"}}' # ACT token = ( beekeeper.api.create_session( - notifications_endpoint=notification_endpoint, salt=salt, ) ).token diff --git a/tests/beekeepy_test/handle/api_tests/test_api_set_timeout.py b/tests/beekeepy_test/handle/api_tests/test_api_set_timeout.py index aba784b3..65f678b8 100644 --- a/tests/beekeepy_test/handle/api_tests/test_api_set_timeout.py +++ b/tests/beekeepy_test/handle/api_tests/test_api_set_timeout.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: def test_api_set_timeout(beekeeper: Beekeeper, wallet: WalletInfo) -> None: # noqa: ARG001 """Test test_api_set_timeout will test beekeeper_api.set_timeout api call.""" # ARRANGE - bk_wallet = (beekeeper.api.list_wallets()).wallets[0] + bk_wallet = (beekeeper.api.list_created_wallets()).wallets[0] assert bk_wallet.unlocked is True, "Wallet should be unlocked." # ACT @@ -20,5 +20,5 @@ def test_api_set_timeout(beekeeper: Beekeeper, wallet: WalletInfo) -> None: # n time.sleep(1.5) # ASSERT - bk_wallet = (beekeeper.api.list_wallets()).wallets[0] + bk_wallet = (beekeeper.api.list_created_wallets()).wallets[0] assert bk_wallet.unlocked is False, "Wallet after timeout should be locked." diff --git a/tests/beekeepy_test/handle/basic/test_wallet.py b/tests/beekeepy_test/handle/basic/test_wallet.py index 56253a75..ef1eac19 100644 --- a/tests/beekeepy_test/handle/basic/test_wallet.py +++ b/tests/beekeepy_test/handle/basic/test_wallet.py @@ -73,13 +73,13 @@ def test_timeout(beekeeper: Beekeeper, wallet: WalletInfo) -> None: # ASSERT info = beekeeper.api.get_info() assert timeout - (info.timeout_time - info.now).total_seconds() <= comparison_error_max_delta - check_wallets(beekeeper.api.list_wallets(), [wallet.name]) + check_wallets(beekeeper.api.list_created_wallets(), [wallet.name]) # ACT time.sleep(timeout + 1) # ASSERT - check_wallets(beekeeper.api.list_wallets(), [wallet.name], unlocked=False) + check_wallets(beekeeper.api.list_created_wallets(), [wallet.name], unlocked=False) @pytest.mark.parametrize("wallet_name", ["test", "123"]) diff --git a/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/config.ini b/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/config.ini index 1b428e92..f55ef484 100644 --- a/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/config.ini +++ b/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/config.ini @@ -1,5 +1,5 @@ # config automatically generated by helpy -wallet-dir=/workspace/hive/tests/python/functional/beekeepy/handle/commandline/application_command_line_options/patterns +wallet-dir=/workspace/helpy/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns unlock-timeout=900 unlock-interval=500 webserver-http-endpoint=0.0.0.0:0 diff --git a/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/generate_help_pattern.py b/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/generate_help_pattern.py index 67224e33..04a03f22 100644 --- a/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/generate_help_pattern.py +++ b/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/generate_help_pattern.py @@ -8,6 +8,9 @@ from beekeepy import Settings from beekeepy._executable.beekeeper_executable import BeekeeperExecutable if __name__ == "__main__": - help_text = BeekeeperExecutable(settings=Settings(), logger=loguru.logger).get_help_text() + settings = Settings() + help_text = BeekeeperExecutable( + executable_path=settings.binary_path, working_directory=settings.ensured_working_directory, logger=loguru.logger + ).get_help_text() help_pattern_file = Path.cwd() / "help_pattern.txt" help_pattern_file.write_text(help_text.strip()) diff --git a/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/help_pattern.txt b/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/help_pattern.txt index 22331b24..b08615a8 100644 --- a/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/help_pattern.txt +++ b/tests/beekeepy_test/handle/commandline/application_command_line_options/patterns/help_pattern.txt @@ -27,9 +27,6 @@ Application Options: --export-keys-wallet "["green-wallet", "PW5KYF9Rt4ETnuP4uheHSCm9kLbCuunf6RqeKgQ8QRoxZmGeZUhhk"]" - --notifications-endpoint arg - list of addresses, that will receive notification about in-chain - events --unlock-interval arg (=500) Protection against unlocking by bots. Every wrong `unlock` enables a delay. By default 500[ms]. @@ -62,5 +59,4 @@ Application Command Line Options: -d [ --data-dir ] dir Directory containing configuration file config.ini. Default location: $HOME/.beekeeper or CWD/. beekeeper -c [ --config ] filename (="config.ini") - Configuration file name relative to data-dir - + Configuration file name relative to data-dir \ No newline at end of file diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_backtrace.py b/tests/beekeepy_test/handle/commandline/application_options/test_backtrace.py index b843da39..8310070f 100644 --- a/tests/beekeepy_test/handle/commandline/application_options/test_backtrace.py +++ b/tests/beekeepy_test/handle/commandline/application_options/test_backtrace.py @@ -6,10 +6,10 @@ from typing import TYPE_CHECKING, Literal import pytest from local_tools.beekeepy import checkers -from beekeepy._executable.arguments.beekeeper_arguments import BeekeeperArguments +from beekeepy.handle.runnable import BeekeeperArguments if TYPE_CHECKING: - from beekeepy._executable.beekeeper_executable import ( + from beekeepy.handle.runnable import ( BeekeeperExecutable, ) @@ -19,9 +19,10 @@ def test_backtrace(backtrace: Literal["yes", "no"], beekeeper_exe: BeekeeperExec """Test will check command line flag --backtrace.""" # ARRAGNE & ACT - with beekeeper_exe.run( + with beekeeper_exe.restore_arguments( + BeekeeperArguments(data_dir=beekeeper_exe.working_directory, backtrace=backtrace) + ), beekeeper_exe.run( blocking=False, - arguments=BeekeeperArguments(data_dir=beekeeper_exe.working_directory, backtrace=backtrace), ): time.sleep(0.1) # ASSERT diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_default_values.py b/tests/beekeepy_test/handle/commandline/application_options/test_default_values.py index 4d7f9eac..53b4c75d 100644 --- a/tests/beekeepy_test/handle/commandline/application_options/test_default_values.py +++ b/tests/beekeepy_test/handle/commandline/application_options/test_default_values.py @@ -18,7 +18,6 @@ def check_default_values_from_config(default_config: BeekeeperConfig) -> None: assert default_config.log_json_rpc == BeekeeperDefaults.DEFAULT_LOG_JSON_RPC assert default_config.webserver_http_endpoint == http_webserver_default() assert default_config.webserver_thread_pool_size == BeekeeperDefaults.DEFAULT_WEBSERVER_THREAD_POOL_SIZE - assert default_config.notifications_endpoint == BeekeeperDefaults.DEFAULT_NOTIFICATIONS_ENDPOINT assert default_config.backtrace == BeekeeperDefaults.DEFAULT_BACKTRACE assert default_config.export_keys_wallet == BeekeeperDefaults.DEFAULT_EXPORT_KEYS_WALLET diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_export_keys_wallet.py b/tests/beekeepy_test/handle/commandline/application_options/test_export_keys_wallet.py index b2cb2f02..b609ec5c 100644 --- a/tests/beekeepy_test/handle/commandline/application_options/test_export_keys_wallet.py +++ b/tests/beekeepy_test/handle/commandline/application_options/test_export_keys_wallet.py @@ -3,8 +3,8 @@ from __future__ import annotations import json from typing import TYPE_CHECKING, Final -from beekeepy._interface.key_pair import KeyPair from beekeepy.handle.runnable import Beekeeper +from beekeepy.interfaces import KeyPair if TYPE_CHECKING: from pathlib import Path @@ -91,4 +91,4 @@ def test_export_keys(beekeeper: Beekeeper) -> None: # ASSERT # Check default path of wallet_name.keys check_dumped_keys(extract_path / wallet_name_keys, keys1) - assert bk.is_running is False + assert bk.is_running() is False diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_log_json_rpc.py b/tests/beekeepy_test/handle/commandline/application_options/test_log_json_rpc.py index af22146a..1e48dbd8 100644 --- a/tests/beekeepy_test/handle/commandline/application_options/test_log_json_rpc.py +++ b/tests/beekeepy_test/handle/commandline/application_options/test_log_json_rpc.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from beekeepy._executable.arguments.beekeeper_arguments import BeekeeperArguments +from beekeepy.handle.runnable import BeekeeperArguments if TYPE_CHECKING: from pathlib import Path diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_notifications_endpoint.py b/tests/beekeepy_test/handle/commandline/application_options/test_notifications_endpoint.py deleted file mode 100644 index 377251ac..00000000 --- a/tests/beekeepy_test/handle/commandline/application_options/test_notifications_endpoint.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - -from beekeepy.handle.runnable import BeekeeperArguments -from beekeepy.interfaces import HttpUrl - -if TYPE_CHECKING: - from beekeepy.handle.runnable import Beekeeper - - -@pytest.mark.parametrize( - "notifications_endpoint", - [HttpUrl("0.0.0.0:0", protocol="http"), HttpUrl("127.0.0.1:0", protocol="http")], -) -def test_notifications_endpoint(beekeeper_not_started: Beekeeper, notifications_endpoint: HttpUrl) -> None: - """ - Test will check command line flag --notifications-endpoint. - - In this test we will re-use built-in notification server. We will not pass a port here, - because built-in notification server will get free port and use it. - - In order to test flag notifications-endpoint we will pass this flag, and internal - we will pass port that already has been taken by notification http server to beekeeper - executable. - """ - # 1 pass notification flag - # 2 inside start function there is special if, that will check if we have explicitly pass - # notification-endpoint flag, and append to it already taken port. This way we will - # point beekeeper where to send notifications. - - # ARRANGE & ACT & ASSERT - beekeeper_not_started.run( - additional_cli_arguments=BeekeeperArguments(notifications_endpoint=notifications_endpoint) - ) diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_unlock_timeout.py b/tests/beekeepy_test/handle/commandline/application_options/test_unlock_timeout.py index b04bb005..0ac7518a 100644 --- a/tests/beekeepy_test/handle/commandline/application_options/test_unlock_timeout.py +++ b/tests/beekeepy_test/handle/commandline/application_options/test_unlock_timeout.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING import pytest -from beekeepy._executable.arguments.beekeeper_arguments import BeekeeperArguments +from beekeepy.handle.runnable import BeekeeperArguments if TYPE_CHECKING: from beekeepy.handle.runnable import Beekeeper @@ -13,7 +13,7 @@ if TYPE_CHECKING: def check_wallet_lock(beekeeper: Beekeeper, required_status: bool) -> None: """Check if wallets are have required unlock status.""" - response_list_wallets = beekeeper.api.list_wallets() + response_list_wallets = beekeeper.api.list_created_wallets() for wallet in response_list_wallets.wallets: assert wallet.unlocked == required_status diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_wallet_dir.py b/tests/beekeepy_test/handle/commandline/application_options/test_wallet_dir.py index ec879fec..0a967be3 100644 --- a/tests/beekeepy_test/handle/commandline/application_options/test_wallet_dir.py +++ b/tests/beekeepy_test/handle/commandline/application_options/test_wallet_dir.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: - from beekeepy._remote_handle.beekeeper import Beekeeper + from beekeepy.handle.remote import Beekeeper def check_wallets_size(beekeeper: Beekeeper, required_size: int) -> None: diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_webserver_http_endpoint.py b/tests/beekeepy_test/handle/commandline/application_options/test_webserver_http_endpoint.py index 7a8e0ce1..ae56ed72 100644 --- a/tests/beekeepy_test/handle/commandline/application_options/test_webserver_http_endpoint.py +++ b/tests/beekeepy_test/handle/commandline/application_options/test_webserver_http_endpoint.py @@ -7,8 +7,8 @@ import pytest import requests from local_tools.beekeepy.network import get_port -from beekeepy._executable.arguments.beekeeper_arguments import BeekeeperArguments -from beekeepy._interface.url import HttpUrl +from beekeepy.handle.runnable import BeekeeperArguments +from beekeepy.interfaces import HttpUrl from schemas.apis import beekeeper_api from schemas.jsonrpc import get_response_model @@ -16,15 +16,12 @@ if TYPE_CHECKING: from beekeepy.handle.runnable import Beekeeper -def check_webserver_http_endpoint(*, nofification_endpoint: HttpUrl, webserver_http_endpoint: HttpUrl) -> None: +def check_webserver_http_endpoint(*, webserver_http_endpoint: HttpUrl) -> None: """Check if beekeeper is listening on given endpoint.""" data = { "jsonrpc": "2.0", "method": "beekeeper_api.create_session", - "params": { - "salt": "avocado", - "notifications_endpoint": nofification_endpoint.as_string(with_protocol=False), - }, + "params": {"salt": "avocado"}, "id": 1, } @@ -49,8 +46,4 @@ def test_webserver_http_endpoint(beekeeper_not_started: Beekeeper, webserver_htt ) # ASSERT - assert beekeeper_not_started.settings.notification_endpoint is not None - check_webserver_http_endpoint( - nofification_endpoint=beekeeper_not_started.settings.notification_endpoint, - webserver_http_endpoint=webserver_http_endpoint, - ) + check_webserver_http_endpoint(webserver_http_endpoint=webserver_http_endpoint) diff --git a/tests/beekeepy_test/handle/commandline/application_options/test_webserver_thread_pool_size.py b/tests/beekeepy_test/handle/commandline/application_options/test_webserver_thread_pool_size.py index 38dbfb82..0f042945 100644 --- a/tests/beekeepy_test/handle/commandline/application_options/test_webserver_thread_pool_size.py +++ b/tests/beekeepy_test/handle/commandline/application_options/test_webserver_thread_pool_size.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING import pytest from local_tools.beekeepy import checkers -from beekeepy._executable.arguments.beekeeper_arguments import BeekeeperArguments +from beekeepy.handle.runnable import BeekeeperArguments if TYPE_CHECKING: from beekeepy.handle.runnable import Beekeeper diff --git a/tests/beekeepy_test/handle/commandline/conftest.py b/tests/beekeepy_test/handle/commandline/conftest.py index 625860c4..c9d68fbe 100644 --- a/tests/beekeepy_test/handle/commandline/conftest.py +++ b/tests/beekeepy_test/handle/commandline/conftest.py @@ -13,4 +13,8 @@ if TYPE_CHECKING: @pytest.fixture() def beekeeper_exe(settings_with_logger: SettingsLoggerFactory) -> BeekeeperExecutable: incoming_settings, logger = settings_with_logger() - return BeekeeperExecutable(settings=incoming_settings, logger=logger) + return BeekeeperExecutable( + executable_path=incoming_settings.binary_path, + working_directory=incoming_settings.ensured_working_directory, + logger=logger, + ) diff --git a/tests/beekeepy_test/handle/conftest.py b/tests/beekeepy_test/handle/conftest.py index 3063076f..434af5ff 100644 --- a/tests/beekeepy_test/handle/conftest.py +++ b/tests/beekeepy_test/handle/conftest.py @@ -27,8 +27,8 @@ def beekeeper_not_started(settings_with_logger: SettingsLoggerFactory) -> Iterat yield bk - if bk.is_running: - bk.teardown() + if bk.is_running(): + bk.close() @pytest.fixture() diff --git a/tests/beekeepy_test/handle/storage/test_storage.py b/tests/beekeepy_test/handle/storage/test_storage.py index e98e83a7..b5712ba6 100644 --- a/tests/beekeepy_test/handle/storage/test_storage.py +++ b/tests/beekeepy_test/handle/storage/test_storage.py @@ -1,17 +1,16 @@ from __future__ import annotations -import json import shutil -from pathlib import Path +from typing import TYPE_CHECKING -import pytest from local_tools.beekeepy import checkers from loguru import logger from beekeepy import Settings -from beekeepy.exceptions import BeekeeperFailedToStartError from beekeepy.handle.runnable import Beekeeper -from beekeepy.interfaces import HttpUrl + +if TYPE_CHECKING: + from pathlib import Path def prepare_directory(path: Path) -> None: @@ -32,17 +31,13 @@ def test_multiply_beekeepeer_same_storage(working_directory: Path) -> None: # ACT & ASSERT 1 with Beekeeper(settings=settings, logger=logger) as bk1: - assert bk1.is_running is True, "First instance of beekeeper should launch without any problems." + assert bk1.is_running() is True, "First instance of beekeeper should launch without any problems." # ACT & ASSERT 2 - bk2 = Beekeeper(settings=settings, logger=logger) - with pytest.raises(BeekeeperFailedToStartError): - bk2.run() - - assert checkers.check_for_pattern_in_file( - bk2.settings.ensured_working_directory / "stderr.log", - "Failed to lock access to wallet directory; is another `beekeeper` running?", - ), "There should be an info about another instance of beekeeper locking wallet directory." + with Beekeeper(settings=settings, logger=logger) as bk2: + assert ( + "opening beekeeper failed" in bk2.apis.app_status.get_app_status().statuses + ), "Second instance of beekeeper should fail to start." def test_multiply_beekeepeer_different_storage(working_directory: Path) -> None: @@ -63,8 +58,8 @@ def test_multiply_beekeepeer_different_storage(working_directory: Path) -> None: settings=Settings(working_directory=bk2_path), logger=logger ) as bk2: # ASSERT - assert bk1.is_running, "First instance of beekeeper should be working." - assert bk2.is_running, "Second instance of beekeeper should be working." + assert bk1.is_running(), "First instance of beekeeper should be working." + assert bk2.is_running(), "Second instance of beekeeper should be working." bks.extend((bk1, bk2)) for bk in bks: @@ -77,44 +72,12 @@ def test_multiply_beekeepeer_different_storage(working_directory: Path) -> None: ), "There should be an no info about another instance of beekeeper locking wallet directory." -def get_remote_address_from_connection_file(working_dir: Path) -> HttpUrl: - connection: dict[str, str | int] = {} - with (working_dir / "beekeeper.connection").open() as file: - connection = json.load(file) - return HttpUrl( - f"{connection['address']}:{connection['port']}", - protocol=str(connection["type"]).lower(), # type: ignore[arg-type] - ) - - def test_beekeepers_files_generation(beekeeper: Beekeeper) -> None: """Test test_beekeepers_files_generation will check if beekeeper files are generated and have same content.""" # ARRANGE & ACT wallet_dir = beekeeper.settings.ensured_working_directory - beekeeper_connection_file = wallet_dir / "beekeeper.connection" - beekeeper_pid_file = wallet_dir / "beekeeper.pid" beekeeper_wallet_lock_file = wallet_dir / "beekeeper.wallet.lock" # ASSERT - assert beekeeper_connection_file.exists() is True, "File 'beekeeper.connection' should exists" - assert beekeeper_pid_file.exists() is True, "File 'beekeeper.pid' should exists" # File beekeeper.wallet.lock holds no value inside, so we need only to check is its exists. assert beekeeper_wallet_lock_file.exists() is True, "File 'beekeeper.wallet.lock' should exists" - - connection_url = get_remote_address_from_connection_file(wallet_dir) - assert connection_url is not None, "There should be connection details." - - if beekeeper.http_endpoint.address == "127.0.0.1": - assert connection_url.address in [ - "0.0.0.0", # noqa: S104 - "127.0.0.1", - ], "Address should point to localhost or all interfaces." - else: - assert connection_url.address == beekeeper.http_endpoint.address, "Host should be the same." - assert connection_url.port == beekeeper.http_endpoint.port, "Port should be the same." - assert connection_url.protocol == beekeeper.http_endpoint.protocol, "Protocol should be the same." - - with Path.open(beekeeper_pid_file) as pid: - content = json.load(pid) - - assert content["pid"] == str(beekeeper.pid), "Pid should be the same" diff --git a/tests/beekeepy_test/handle/various/test_blocking_unlock.py b/tests/beekeepy_test/handle/various/test_blocking_unlock.py index 8eb8edd3..a6961de8 100644 --- a/tests/beekeepy_test/handle/various/test_blocking_unlock.py +++ b/tests/beekeepy_test/handle/various/test_blocking_unlock.py @@ -8,12 +8,12 @@ from local_tools.beekeepy.generators import default_wallet_credentials from local_tools.beekeepy.models import SettingsLoggerFactory, WalletInfo from local_tools.beekeepy.network import async_raw_http_call -from beekeepy._interface.delay_guard import DelayGuardBase +from beekeepy._utilities.delay_guard import DelayGuardBase from beekeepy.handle.runnable import AsyncBeekeeper from schemas.jsonrpc import JSONRPCRequest if TYPE_CHECKING: - from beekeepy._interface.url import HttpUrl as Url + from beekeepy.interfaces import HttpUrl as Url # We have 500ms time period protection on ulocking wallet. @@ -29,8 +29,8 @@ def beekeeper_not_started(settings_with_logger: SettingsLoggerFactory) -> Iterat yield bk - if bk.is_running: - bk.teardown() + if bk.is_running(): + bk.close() @pytest.fixture() @@ -98,14 +98,8 @@ async def test_wallet_blocking_timeout(beekeeper: AsyncBeekeeper, wallet: Wallet assert wallets[0].name == wallet.name unlock_jsons = [] - assert beekeeper.settings.notification_endpoint is not None for i in range(5): - session = ( - await beekeeper.api.create_session( - notifications_endpoint=beekeeper.settings.notification_endpoint.as_string(with_protocol=False), - salt=f"salt-{i}", - ) - ).token + session = (await beekeeper.api.create_session(salt=f"salt-{i}")).token unlock_json = JSONRPCRequest( method="beekeeper_api.unlock", params={ diff --git a/tests/beekeepy_test/interface/test_setup.py b/tests/beekeepy_test/interface/test_setup.py index 9680c4cb..a8607533 100644 --- a/tests/beekeepy_test/interface/test_setup.py +++ b/tests/beekeepy_test/interface/test_setup.py @@ -2,7 +2,10 @@ from __future__ import annotations from typing import TYPE_CHECKING +import pytest + from beekeepy import Beekeeper +from beekeepy.exceptions import InvalidatedStateByClosingBeekeeperError, InvalidatedStateByClosingSessionError if TYPE_CHECKING: from local_tools.beekeepy.models import SettingsFactory @@ -10,19 +13,20 @@ if TYPE_CHECKING: def test_closing_with_delete(settings: SettingsFactory) -> None: # ARRANGE - sets = settings() - bk = Beekeeper.factory(settings=sets) + bk = Beekeeper.factory(settings=settings()) # ACT & ASSERT (no throw) bk.teardown() - assert not (sets.ensured_working_directory / "beekeeper.pid").exists() + with pytest.raises(InvalidatedStateByClosingBeekeeperError): + bk.create_session() def test_closing_with_with(settings: SettingsFactory) -> None: # ARRANGE, ACT & ASSERT (no throw) - sets = settings() - with Beekeeper.factory(settings=sets): - assert (sets.ensured_working_directory / "beekeeper.pid").exists() + with Beekeeper.factory(settings=settings()) as bk, bk.create_session() as session: + pass + with pytest.raises(InvalidatedStateByClosingSessionError): + session.wallets # noqa: B018 # part of test def test_session_tokens(settings: SettingsFactory) -> None: diff --git a/tests/beekeepy_test/interface/test_standalone_beekeeper.py b/tests/beekeepy_test/interface/test_standalone_beekeeper.py index 47f5833d..265cc1eb 100644 --- a/tests/beekeepy_test/interface/test_standalone_beekeeper.py +++ b/tests/beekeepy_test/interface/test_standalone_beekeeper.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import os import sys import time @@ -30,8 +29,7 @@ def verify_beekeeper_status(path_or_pid: Path | int, alive: bool) -> int: pid: int | None = None if isinstance(path_or_pid, Path): assert path_or_pid.exists(), f"Beekeeper started too slow, missing file: {path_or_pid.as_posix()}" - with path_or_pid.open("r") as rfile: - pid = int(json.load(rfile).get("pid", -1)) + pid = int(path_or_pid.read_text().strip()) else: pid = path_or_pid @@ -45,7 +43,7 @@ def test_standalone_beekeeper(working_directory: Path) -> None: path_to_resource_directory = Path(__file__).resolve().parent / "resources" path_to_script = path_to_resource_directory / "standalone_beekeeper_by_args.py" path_to_working_directory = working_directory / "wdir" - path_to_pid_file = path_to_working_directory / "beekeeper.pid" + path_to_pid_file = path_to_working_directory / "pid.txt" # ACT & ASSERT run_python_script( diff --git a/tests/conftest.py b/tests/conftest.py index d0847cce..0e1c0b05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest -from beekeepy._remote_handle.abc.api import AbstractApi, RegisteredApisT +from beekeepy.handle.remote import AbstractSyncApi, RegisteredApisT from beekeepy.interfaces import HttpUrl @@ -30,12 +30,14 @@ def _convert_test_name_to_directory_name(test_name: str) -> str: @pytest.fixture(autouse=True) def working_directory(request: pytest.FixtureRequest) -> Path: name_of_directory = _convert_test_name_to_directory_name(request.node.name) - path_to_generated = request.node.path.parent / name_of_directory - if path_to_generated.exists(): - shutil.rmtree(path_to_generated) - path_to_generated.mkdir() - assert isinstance(path_to_generated, Path), "given object is not Path" - return path_to_generated + path_to_module_generated = request.node.path.parent / f"generated_{request.node.path.stem}" + path_to_module_generated.mkdir(exist_ok=True) + path_to_test_artifacts = path_to_module_generated / name_of_directory + if path_to_test_artifacts.exists(): + shutil.rmtree(path_to_test_artifacts) + path_to_test_artifacts.mkdir() + assert isinstance(path_to_test_artifacts, Path), "given object is not Path" + return path_to_test_artifacts def pytest_addoption(parser: pytest.Parser) -> None: @@ -47,7 +49,7 @@ def pytest_addoption(parser: pytest.Parser) -> None: @pytest.fixture() def registered_apis() -> RegisteredApisT: """Return registered methods.""" - return AbstractApi._get_registered_methods() + return AbstractSyncApi._get_registered_methods() @pytest.fixture() diff --git a/tests/local-tools/local_tools/beekeepy/account_credentials.py b/tests/local-tools/local_tools/beekeepy/account_credentials.py index a30be4bf..0f230121 100644 --- a/tests/local-tools/local_tools/beekeepy/account_credentials.py +++ b/tests/local-tools/local_tools/beekeepy/account_credentials.py @@ -3,7 +3,7 @@ from __future__ import annotations import random from typing import Final -from beekeepy._interface.key_pair import KeyPair +from beekeepy._executable import KeyPair ACCOUNTS_DATA: Final[list[dict[str, str]]] = [ { diff --git a/tests/local-tools/local_tools/beekeepy/network.py b/tests/local-tools/local_tools/beekeepy/network.py index 2daa290b..d9de2944 100644 --- a/tests/local-tools/local_tools/beekeepy/network.py +++ b/tests/local-tools/local_tools/beekeepy/network.py @@ -4,18 +4,17 @@ import socket from json import loads from typing import TYPE_CHECKING, Any -from beekeepy import Settings -from beekeepy._communication.aiohttp_communicator import AioHttpCommunicator -from beekeepy._communication.request_communicator import RequestCommunicator +from beekeepy._communication import AioHttpCommunicator, RequestCommunicator +from beekeepy._remote_handle import RemoteHandleSettings if TYPE_CHECKING: - from beekeepy._interface.url import HttpUrl + from beekeepy._communication import HttpUrl from schemas.jsonrpc import JSONRPCRequest async def async_raw_http_call(*, http_endpoint: HttpUrl, data: JSONRPCRequest) -> dict[str, Any]: """Make raw call with given data to given http_endpoint.""" - communicator = AioHttpCommunicator(settings=Settings(http_endpoint=http_endpoint)) + communicator = AioHttpCommunicator(settings=RemoteHandleSettings(http_endpoint=http_endpoint)) response = await communicator.async_send(url=http_endpoint, data=data.json(by_alias=True)) parsed = loads(response) assert isinstance(parsed, dict), "expected json object" @@ -24,7 +23,7 @@ async def async_raw_http_call(*, http_endpoint: HttpUrl, data: JSONRPCRequest) - def raw_http_call(*, http_endpoint: HttpUrl, data: JSONRPCRequest) -> dict[str, Any]: """Make raw call with given data to given http_endpoint.""" - communicator = RequestCommunicator(settings=Settings(http_endpoint=http_endpoint)) + communicator = RequestCommunicator(settings=RemoteHandleSettings(http_endpoint=http_endpoint)) response = communicator.send(url=http_endpoint, data=data.json(by_alias=True)) parsed = loads(response) assert isinstance(parsed, dict), "expected json object" diff --git a/tests/local-tools/poetry.lock b/tests/local-tools/poetry.lock index f8973037..baa11fd2 100644 --- a/tests/local-tools/poetry.lock +++ b/tests/local-tools/poetry.lock @@ -175,10 +175,10 @@ develop = true aiohttp = "3.9.1" httpx = {version = "0.23.3", extras = ["http2"]} loguru = "0.7.2" -pydantic = "1.10.18" +psutil = "6.0.0" python-dateutil = "2.8.2" requests = "2.27.1" -schemas = "0.0.1.dev323+e5a1ba1" +schemas = "0.0.1.dev333+540050d" [package.source] type = "directory" @@ -755,6 +755,35 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "psutil" +version = "6.0.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, + {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, + {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, + {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + [[package]] name = "pydantic" version = "1.10.18" @@ -1017,12 +1046,12 @@ files = [ [[package]] name = "schemas" -version = "0.0.1.dev323+e5a1ba1" +version = "0.0.1.dev333+540050d" description = "Tools for checking if message fits expected format" optional = false python-versions = ">=3.10,<4.0" files = [ - {file = "schemas-0.0.1.dev323+e5a1ba1-py3-none-any.whl", hash = "sha256:b97546f24f54f58d71fade176e3fdfa8e113c040d35f56f04e5a567b0fa63d5f"}, + {file = "schemas-0.0.1.dev333+540050d-py3-none-any.whl", hash = "sha256:0bdaab260640262a8b4b64e1a4c9fb7aefcedc0f418eb1a646fbad491a934ac7"}, ] [package.dependencies] @@ -1066,6 +1095,17 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-psutil" +version = "6.0.0.20240901" +description = "Typing stubs for psutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-psutil-6.0.0.20240901.tar.gz", hash = "sha256:437affa76670363db9ffecfa4f153cc6900bf8a7072b3420f3bc07a593f92226"}, + {file = "types_psutil-6.0.0.20240901-py3-none-any.whl", hash = "sha256:20af311bfb0386a018a27ae47dc952119d7c0e849ff72b6aa24fc0433afb92a6"}, +] + [[package]] name = "types-python-dateutil" version = "2.8.19.14" @@ -1282,4 +1322,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "b04ca12a352bb62bdc19b6b7424482f8e49b950d17749b2b188ceecbcd5a47d5" +content-hash = "ff055be77c71e2fbfc606e1993e4019e1a37b56e91fcd580c4934a7c2f9a9559" diff --git a/tests/local-tools/pyproject.toml b/tests/local-tools/pyproject.toml index 07b8d6d9..634ae01e 100644 --- a/tests/local-tools/pyproject.toml +++ b/tests/local-tools/pyproject.toml @@ -36,6 +36,7 @@ ruff = "0.4.9" types-python-dateutil = "2.8.19.14" types-pyyaml = "6.0.12.4" types-requests = "2.31.0.2" +types-psutil = "6.0.0.20240901" [tool.mypy] -- GitLab