diff --git a/.flake8 b/.flake8 index d1c63f4..3dbe748 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,4 @@ [flake8] max-line-length = 120 -ignore = D,E800,F401,F403,F405,I,N818,Q000,RST210,RST213,RST304,WPS,W503 +ignore = C812,C813,C815,C816,D,E800,F401,F403,F405,I,N818,Q000,RST210,RST213,RST304,WPS,W503 +classmethod-decorators=classmethod,validator diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81c0426..121cfe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,30 @@ jobs: - name: Check code formatting with black run: black -l 120 . --diff --check + mypy: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + with: + submodules: recursive + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install mypy + run: | + pip install --upgrade pip + pip install -r requirements.txt + pip install mypy types-python-dateutil types-requests + + - name: Check typing with mypy + run: mypy PyCrypCli + linter: runs-on: ubuntu-latest @@ -219,7 +243,7 @@ jobs: pypi: runs-on: ubuntu-latest - needs: [ codestyle, linter, build, docker_build ] + needs: [ codestyle, mypy, linter, build, docker_build ] if: ${{ startsWith(github.ref, 'refs/tags/v') }} environment: pypi @@ -240,7 +264,7 @@ jobs: docker_push_ghcr: if: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }} - needs: [ codestyle, linter, build, docker_build ] + needs: [ codestyle, mypy, linter, build, docker_build ] runs-on: ubuntu-latest steps: diff --git a/Dockerfile b/Dockerfile index 6acb7a8..9c968f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,6 @@ COPY requirements.txt /app/ RUN pip install -r requirements.txt -COPY pycrypcli.py /app/ COPY PyCrypCli /app/PyCrypCli -CMD ["python", "pycrypcli.py"] +CMD ["python", "-m", "PyCrypCli"] diff --git a/PyCrypCli/__main__.py b/PyCrypCli/__main__.py new file mode 100755 index 0000000..c318317 --- /dev/null +++ b/PyCrypCli/__main__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +from .pycrypcli import main + +main() diff --git a/PyCrypCli/client.py b/PyCrypCli/client.py index 0c0e6ec..f3cc6fc 100644 --- a/PyCrypCli/client.py +++ b/PyCrypCli/client.py @@ -2,25 +2,31 @@ import re import ssl import time -from typing import List, Optional, Type +from os import getenv +from typing import Type, Any, cast from uuid import uuid4 import sentry_sdk +from pydantic import ValidationError from websocket import WebSocket, create_connection -from PyCrypCli.exceptions import ( - UnknownMicroserviceException, - InvalidServerResponseException, +from .exceptions import ( + UnknownMicroserviceError, + InvalidServerResponseError, MicroserviceException, - WeakPasswordException, - UsernameAlreadyExistsException, - InvalidLoginException, - InvalidSessionTokenException, - PermissionsDeniedException, - LoggedInException, - LoggedOutException, + WeakPasswordError, + UsernameAlreadyExistsError, + InvalidLoginError, + InvalidSessionTokenError, + PermissionDeniedError, + LoggedInError, + LoggedOutError, + ClientNotReadyError, ) -from PyCrypCli.timer import Timer +from .models import HardwareConfig, StatusResponse, InfoResponse, TokenResponse +from .timer import Timer + +LOG_WS = bool(getenv("LOG_WS")) def uuid() -> str: @@ -30,184 +36,220 @@ def uuid() -> str: class Client: def __init__(self, server: str): self.server: str = server - self.websocket: Optional[WebSocket] = None - self.timer: Optional[Timer] = None + self.websocket: WebSocket | None = None + self.timer: Timer | None = None self.waiting_for_response: bool = False - self.notifications: List[dict] = [] + self.notifications: list[dict[str, Any]] = [] self.logged_in: bool = False - def init(self): + def init(self) -> None: try: - self.websocket: WebSocket = create_connection(self.server) + self.websocket = create_connection(self.server) except ssl.SSLCertVerificationError: - self.websocket: WebSocket = create_connection(self.server, sslopt={"cert_reqs": ssl.CERT_NONE}) - self.timer: Timer = Timer(10, self.info) + self.websocket = create_connection(self.server, sslopt={"cert_reqs": ssl.CERT_NONE}) + self.timer = Timer(10, self.info) - def close(self): - if self.timer is not None: + def close(self) -> None: + if self.timer: self.timer.stop() self.timer = None - self.websocket.close() - self.websocket = None - self.logged_in: bool = False + if self.websocket: + self.websocket.close() + self.websocket = None + + self.logged_in = False + + def _send(self, obj: dict[str, Any]) -> None: + if not self.websocket: + raise ClientNotReadyError + + data = json.dumps(obj) + if LOG_WS: + print("send:", data) + sentry_sdk.add_breadcrumb(category="ws", message=f"send: {data}", level="debug") + self.websocket.send(data) + + def _recv(self) -> dict[str, Any]: + if not self.websocket: + raise ClientNotReadyError - def request(self, data: dict, no_response: bool = False) -> dict: + data = self.websocket.recv() + if LOG_WS: + print("recv:", data) + sentry_sdk.add_breadcrumb(category="ws", message=f"recv: {data}", level="debug") + return cast(dict[str, Any], json.loads(data)) + + def request(self, data: dict[str, Any], no_response: bool = False) -> dict[str, Any]: if self.websocket is None: raise ConnectionError while self.waiting_for_response: time.sleep(0.01) - self.waiting_for_response: bool = True - data = json.dumps(data) - sentry_sdk.add_breadcrumb(category="ws", message=f"send: {data}", level="debug") - self.websocket.send(data) + self.waiting_for_response = True + + self._send(data) + if no_response: - self.waiting_for_response: bool = False + self.waiting_for_response = False return {} + while True: - data = self.websocket.recv() - sentry_sdk.add_breadcrumb(category="ws", message=f"recv: {data}", level="debug") - response: dict = json.loads(data) + response = self._recv() if "notify-id" in response: self.notifications.append(response) else: break - self.waiting_for_response: bool = False + + self.waiting_for_response = False return response - def ms(self, ms: str, endpoint: List[str], **data) -> dict: + def ms(self, ms: str, endpoint: list[str], *, retry: int = 0, **data: Any) -> dict[str, Any]: if not self.logged_in: - raise LoggedOutException + raise LoggedOutError - response: dict = self.request({"ms": ms, "endpoint": endpoint, "data": data, "tag": uuid()}) + response: dict[str, Any] = self.request({"ms": ms, "endpoint": endpoint, "data": data, "tag": uuid()}) if "error" in response: error: str = response["error"] if error == "unknown microservice": - raise UnknownMicroserviceException(ms) - raise InvalidServerResponseException(response) + raise UnknownMicroserviceError(ms) + raise InvalidServerResponseError(response) if "data" not in response: - raise InvalidServerResponseException(response) - - data: dict = response["data"] - if "error" in data: - error: str = data["error"] - for exception in MicroserviceException.__subclasses__(): # type: Type[MicroserviceException] - match = re.fullmatch(exception.error, error) - if match: + raise InvalidServerResponseError(response) + + response_data: dict[str, Any] = response["data"] + if "error" in response_data: + error = response_data["error"] + exception: Type[MicroserviceException] + for exception in MicroserviceException.__subclasses__(): + if exception.error and (match := re.fullmatch(exception.error, error)): raise exception(error, list(match.groups())) - raise InvalidServerResponseException(response) - return data + raise InvalidServerResponseError(response) + + if not response_data and retry: + return self.ms(ms, endpoint, retry=retry - 1, **data) - def register(self, username: str, password: str) -> str: + return response_data + + def register(self, username: str, password: str) -> TokenResponse: if self.logged_in: - raise LoggedInException + raise LoggedInError self.init() - response: dict = self.request({"action": "register", "name": username, "password": password}) + response: dict[str, Any] = self.request({"action": "register", "name": username, "password": password}) if "error" in response: self.close() error: str = response["error"] if error == "invalid password": - raise WeakPasswordException() + raise WeakPasswordError() if error == "username already exists": - raise UsernameAlreadyExistsException() - raise InvalidServerResponseException(response) - if "token" not in response: + raise UsernameAlreadyExistsError() + raise InvalidServerResponseError(response) + + try: + token_response = TokenResponse.parse(self, response) + except ValidationError: self.close() - raise InvalidServerResponseException(response) - self.logged_in: bool = True - self.timer.start() - return response["token"] + raise + + self.logged_in = True + cast(Timer, self.timer).start() + return token_response - def login(self, username: str, password: str) -> str: + def login(self, username: str, password: str) -> TokenResponse: if self.logged_in: - raise LoggedInException + raise LoggedInError self.init() - response: dict = self.request({"action": "login", "name": username, "password": password}) + response: dict[str, Any] = self.request({"action": "login", "name": username, "password": password}) if "error" in response: self.close() error: str = response["error"] if error == "permissions denied": - raise InvalidLoginException() - raise InvalidServerResponseException(response) - if "token" not in response: + raise InvalidLoginError() + raise InvalidServerResponseError(response) + + try: + token_response = TokenResponse.parse(self, response) + except ValidationError: self.close() - raise InvalidServerResponseException(response) - self.logged_in: bool = True - self.timer.start() - return response["token"] + raise - def session(self, token: str): + self.logged_in = True + cast(Timer, self.timer).start() + return token_response + + def session(self, token: str) -> TokenResponse: if self.logged_in: - raise LoggedInException + raise LoggedInError self.init() - response: dict = self.request({"action": "session", "token": token}) + response: dict[str, Any] = self.request({"action": "session", "token": token}) if "error" in response: self.close() error: str = response["error"] if error == "invalid token": - raise InvalidSessionTokenException() - raise InvalidServerResponseException(response) - if "token" not in response: + raise InvalidSessionTokenError() + raise InvalidServerResponseError(response) + + try: + token_response = TokenResponse.parse(self, response) + except ValidationError: self.close() - raise InvalidServerResponseException(response) - self.logged_in: bool = True - self.timer.start() + raise + + self.logged_in = True + cast(Timer, self.timer).start() + return token_response - def change_password(self, old_password: str, new_password: str) -> str: + def change_password(self, old_password: str, new_password: str) -> TokenResponse: if not self.logged_in: - raise LoggedOutException + raise LoggedOutError - response: dict = self.request( - {"action": "password", "password": old_password, "new": new_password}, - ) + response: dict[str, Any] = self.request({"action": "password", "password": old_password, "new": new_password}) if "error" in response: error: str = response["error"] if error == "permissions denied": - raise PermissionsDeniedException() - raise InvalidServerResponseException(response) - if "token" not in response: - raise InvalidServerResponseException(response) - return response["token"] + raise PermissionDeniedError() + raise InvalidServerResponseError(response) - def logout(self): + return TokenResponse.parse(self, response) + + def logout(self) -> None: if not self.logged_in: - raise LoggedOutException + raise LoggedOutError self.request({"action": "logout"}) self.close() - def status(self) -> dict: + def status(self) -> StatusResponse: if self.logged_in: - raise LoggedInException + raise LoggedInError self.init() - response: dict = self.request({"action": "status"}) + response: dict[str, Any] = self.request({"action": "status"}) self.close() if "error" in response: - raise InvalidServerResponseException(response) - return response + raise InvalidServerResponseError(response) + return StatusResponse.parse(self, response) - def info(self) -> dict: + def info(self) -> InfoResponse: if not self.logged_in: - raise LoggedOutException + raise LoggedOutError - response: dict = self.request({"action": "info"}) + response: dict[str, Any] = self.request({"action": "info"}) if "error" in response: - raise InvalidServerResponseException(response) - return response + raise InvalidServerResponseError(response) + return InfoResponse.parse(self, response) - def delete_user(self): + def delete_user(self) -> None: if not self.logged_in: - raise LoggedOutException + raise LoggedOutError self.request({"action": "delete"}, no_response=True) self.close() - def get_hardware_config(self) -> dict: - return self.ms("device", ["hardware", "list"]) + def get_hardware_config(self) -> HardwareConfig: + return HardwareConfig.parse(self, self.ms("device", ["hardware", "list"])) diff --git a/PyCrypCli/commands/__init__.py b/PyCrypCli/commands/__init__.py index 6b05ca9..25a5bea 100644 --- a/PyCrypCli/commands/__init__.py +++ b/PyCrypCli/commands/__init__.py @@ -1,3 +1,4 @@ -from PyCrypCli.commands.command import command, make_commands, commands, Command, CommandError +from .command import command, make_commands, commands, Command, CommandError + __all__ = ["command", "make_commands", "commands", "Command", "CommandError"] diff --git a/PyCrypCli/commands/account.py b/PyCrypCli/commands/account.py index 8af531f..68cd547 100644 --- a/PyCrypCli/commands/account.py +++ b/PyCrypCli/commands/account.py @@ -1,18 +1,14 @@ import getpass import sys +from typing import Any -from PyCrypCli.commands import command, CommandError -from PyCrypCli.context import LoginContext, MainContext, DeviceContext -from PyCrypCli.exceptions import ( - WeakPasswordException, - UsernameAlreadyExistsException, - InvalidLoginException, - PermissionsDeniedException, -) +from .command import command, CommandError +from ..context import LoginContext, MainContext, DeviceContext +from ..exceptions import WeakPasswordError, UsernameAlreadyExistsError, InvalidLoginError, PermissionDeniedError @command("register", [LoginContext], aliases=["signup"]) -def register(context: LoginContext, *_): +def register(context: LoginContext, _: Any) -> None: """ Create a new account """ @@ -27,16 +23,16 @@ def register(context: LoginContext, *_): if password != confirm_password: raise CommandError("Passwords don't match.") try: - session_token: str = context.client.register(username, password) + session_token: str = context.client.register(username, password).token context.open(MainContext(context.root_context, session_token)) - except WeakPasswordException: + except WeakPasswordError: raise CommandError("Password is too weak.") - except UsernameAlreadyExistsException: + except UsernameAlreadyExistsError: raise CommandError("Username already exists.") @command("login", [LoginContext]) -def login(context: LoginContext, *_): +def login(context: LoginContext, _: Any) -> None: """ Login with an existing account """ @@ -48,14 +44,14 @@ def login(context: LoginContext, *_): raise CommandError("\nAborted.") try: - session_token: str = context.client.login(username, password) + session_token: str = context.client.login(username, password).token context.open(MainContext(context.root_context, session_token)) - except InvalidLoginException: + except InvalidLoginError: raise CommandError("Invalid Login Credentials.") @command("exit", [LoginContext], aliases=["quit"]) -def handle_login_exit(*_): +def handle_login_exit(*_: Any) -> None: """ Exit PyCrypCli """ @@ -64,7 +60,7 @@ def handle_login_exit(*_): @command("exit", [MainContext], aliases=["quit"]) -def handle_main_exit(context: MainContext, *_): +def handle_main_exit(context: MainContext, _: Any) -> None: """ Exit PyCrypCli (session will be saved) """ @@ -74,7 +70,7 @@ def handle_main_exit(context: MainContext, *_): @command("exit", [DeviceContext], aliases=["quit", "logout"]) -def handle_device_exit(context: DeviceContext, *_): +def handle_device_exit(context: DeviceContext, _: Any) -> None: """ Disconnect from this device """ @@ -83,7 +79,7 @@ def handle_device_exit(context: DeviceContext, *_): @command("logout", [MainContext]) -def handle_main_logout(context: MainContext, *_): +def handle_main_logout(context: MainContext, _: Any) -> None: """ Delete the current session and exit PyCrypCli """ @@ -92,7 +88,7 @@ def handle_main_logout(context: MainContext, *_): @command("passwd", [MainContext]) -def handle_passwd(context: MainContext, *_): +def handle_passwd(context: MainContext, _: Any) -> None: """ Change your password """ @@ -105,15 +101,15 @@ def handle_passwd(context: MainContext, *_): raise CommandError("Passwords don't match.") try: - context.session_token = context.client.change_password(old_password, new_password) + context.session_token = context.client.change_password(old_password, new_password).token context.save_session() print("Password updated successfully.") - except PermissionsDeniedException: + except PermissionDeniedError: raise CommandError("Incorrect password or the new password does not meet the requirements.") @command("_delete_user", [MainContext]) -def handle_delete_user(context: MainContext, *_): +def handle_delete_user(context: MainContext, _: Any) -> None: """ Delete this account """ diff --git a/PyCrypCli/commands/command.py b/PyCrypCli/commands/command.py index e6c2edc..43bb4ba 100644 --- a/PyCrypCli/commands/command.py +++ b/PyCrypCli/commands/command.py @@ -1,8 +1,10 @@ +from __future__ import annotations + from importlib import import_module -from typing import Callable, List, Dict, Type, Optional, Tuple +from typing import Callable, Type -from PyCrypCli.context import Context, COMMAND_FUNCTION, COMPLETER_FUNCTION -from PyCrypCli.exceptions import CommandRegistrationException, NoDocStringException +from ..context import Context, COMMAND_FUNCTION, COMPLETER_FUNCTION, ContextType +from ..exceptions import CommandRegistrationError, NoDocStringError class CommandError(Exception): @@ -15,40 +17,40 @@ class Command: def __init__( self, name: str, - func: COMMAND_FUNCTION, + func: COMMAND_FUNCTION[ContextType], description: str, - contexts: List[Type[Context]], - aliases: List[str], + contexts: list[Type[Context]], + aliases: list[str], ): self.name: str = name - self.func: COMMAND_FUNCTION = func + self.func: COMMAND_FUNCTION[ContextType] = func self.description: str = description - self.contexts: List[Type[Context]] = contexts - self.aliases: List[str] = aliases - self.completer_func: Optional[COMPLETER_FUNCTION] = None - self.subcommands: List[Command] = [] - self.prepared_subcommands: Dict[Type[Context], Dict[str, Command]] = {} + self.contexts: list[Type[Context]] = contexts + self.aliases: list[str] = aliases + self.completer_func: COMPLETER_FUNCTION[ContextType] | None = None + self.subcommands: list[Command] = [] + self.prepared_subcommands: dict[Type[Context], dict[str, Command]] = {} - def __call__(self, *args, **kwargs): - return self.func(*args, **kwargs) + def __call__(self, context: ContextType, args: list[str]) -> None: + self.func(context, args) - def parse_command(self, context: Context, args: List[str]) -> Tuple["Command", List[str]]: + def parse_command(self, context: Context, args: list[str]) -> tuple[Command, list[str]]: prepared_subcommands = self.prepared_subcommands.get(type(context), {}) if args: - cmd: Optional[Command] = prepared_subcommands.get(args[0]) + cmd: Command | None = prepared_subcommands.get(args[0]) if cmd is not None: return cmd.parse_command(context, args[1:]) return self, args - def handle(self, context: Context, args: List[str]): + def handle(self, context: ContextType, args: list[str]) -> None: cmd, args = self.parse_command(context, args) try: cmd.func(context, args) except CommandError as error: print(error.msg) - def handle_completer(self, context: Context, args: List[str]) -> List[str]: + def handle_completer(self, context: ContextType, args: list[str]) -> list[str]: cmd, args = self.parse_command(context, args) out = list(cmd.prepared_subcommands.get(type(context), {})) @@ -57,55 +59,46 @@ def handle_completer(self, context: Context, args: List[str]) -> List[str]: return out - def completer(self): - def decorator(func: COMPLETER_FUNCTION) -> COMPLETER_FUNCTION: + def completer(self) -> Callable[[COMPLETER_FUNCTION[ContextType]], COMPLETER_FUNCTION[ContextType]]: + def decorator(func: COMPLETER_FUNCTION[ContextType]) -> COMPLETER_FUNCTION[ContextType]: self.completer_func = func return func return decorator def subcommand( - self, - name: str, - *, - contexts: List[Type[Context]] = None, - aliases: List[str] = None, - ) -> Callable[[COMMAND_FUNCTION], "Command"]: - if contexts is None: - contexts = self.contexts - - def decorator(func: COMMAND_FUNCTION) -> Command: + self, name: str, *, contexts: list[Type[Context]] | None = None, aliases: list[str] | None = None + ) -> Callable[[COMMAND_FUNCTION[ContextType]], "Command"]: + def decorator(func: COMMAND_FUNCTION[ContextType]) -> Command: if func.__doc__ is None: - raise NoDocStringException(name, subcommand=True) + raise NoDocStringError(name, subcommand=True) desc: str = func.__doc__ desc = "\n".join(map(str.strip, desc.splitlines())).strip() - cmd = Command(name, func, desc, contexts, aliases or []) + cmd = Command(name, func, desc, contexts or self.contexts, aliases or []) self.subcommands.append(cmd) return cmd return decorator - def make_subcommands(self): + def make_subcommands(self) -> None: for cmd in self.subcommands: for context in cmd.contexts: for name in [cmd.name] + cmd.aliases: if name in self.prepared_subcommands.setdefault(context, {}): - raise CommandRegistrationException(name, subcommand=True) + raise CommandRegistrationError(name, subcommand=True) self.prepared_subcommands[context][name] = cmd cmd.make_subcommands() -commands: List[Command] = [] +commands: list[Command] = [] def command( - name: str, - contexts: List[Type[Context]], - aliases: List[str] = None, -) -> Callable[[COMMAND_FUNCTION], Command]: - def decorator(func: COMMAND_FUNCTION) -> Command: + name: str, contexts: list[Type[Context]], aliases: list[str] | None = None +) -> Callable[[COMMAND_FUNCTION[ContextType]], Command]: + def decorator(func: COMMAND_FUNCTION[ContextType]) -> Command: if func.__doc__ is None: - raise NoDocStringException(name) + raise NoDocStringError(name) desc: str = func.__doc__ desc = "\n".join(map(str.strip, desc.splitlines())).strip() cmd = Command(name, func, desc, contexts, aliases or []) @@ -115,7 +108,7 @@ def decorator(func: COMMAND_FUNCTION) -> Command: return decorator -def make_commands() -> Dict[Type[Context], Dict[str, Command]]: +def make_commands() -> dict[Type[Context], dict[str, Command]]: for module in [ "account", "help", @@ -132,12 +125,12 @@ def make_commands() -> Dict[Type[Context], Dict[str, Command]]: ]: import_module(f"PyCrypCli.commands.{module}") - result: Dict[Type[Context], Dict[str, Command]] = {} + result: dict[Type[Context], dict[str, Command]] = {} for cmd in commands: for context in cmd.contexts: for name in [cmd.name] + cmd.aliases: if name in result.setdefault(context, {}): - raise CommandRegistrationException(name) + raise CommandRegistrationError(name) result[context][name] = cmd cmd.make_subcommands() return result diff --git a/PyCrypCli/commands/device.py b/PyCrypCli/commands/device.py index 245d034..ae3891f 100644 --- a/PyCrypCli/commands/device.py +++ b/PyCrypCli/commands/device.py @@ -1,28 +1,29 @@ -from typing import List, Dict, Optional - -from PyCrypCli.commands import command, CommandError -from PyCrypCli.commands.help import print_help -from PyCrypCli.context import MainContext, DeviceContext -from PyCrypCli.exceptions import ( - AlreadyOwnADeviceException, - DeviceNotFoundException, - IncompatibleCPUSocket, - NotEnoughRAMSlots, - IncompatibleRAMTypes, - IncompatibleDriverInterface, +from typing import Any, cast + +from .command import command, CommandError +from .help import print_help +from ..context import MainContext, DeviceContext +from ..exceptions import ( + AlreadyOwnADeviceError, + DeviceNotFoundError, + IncompatibleCPUSocketError, + NotEnoughRAMSlotsError, + IncompatibleRAMTypesError, + IncompatibleDriverInterfaceError, + DeviceIsStarterDeviceError, ) -from PyCrypCli.game_objects import Device, ResourceUsage, DeviceHardware, InventoryElement -from PyCrypCli.util import is_uuid +from ..models import Device, ResourceUsage, DeviceHardware, InventoryElement, HardwareConfig +from ..util import is_uuid -def get_device(context: MainContext, name_or_uuid: str, devices: Optional[List[Device]] = None) -> Device: +def get_device(context: MainContext, name_or_uuid: str, devices: list[Device] | None = None) -> Device: if is_uuid(name_or_uuid): try: return Device.get_device(context.client, name_or_uuid) - except DeviceNotFoundException: + except DeviceNotFoundError: raise CommandError(f"There is no device with the uuid '{name_or_uuid}'.") else: - found_devices: List[Device] = [] + found_devices: list[Device] = [] for device in devices or Device.list_devices(context.client): if device.name == name_or_uuid: found_devices.append(device) @@ -30,13 +31,13 @@ def get_device(context: MainContext, name_or_uuid: str, devices: Optional[List[D raise CommandError(f"There is no device with the name '{name_or_uuid}'.") if len(found_devices) > 1: raise CommandError( - f"There is more than one device with the name '{name_or_uuid}'. You need to specify its UUID.", + f"There is more than one device with the name '{name_or_uuid}'. You need to specify its UUID." ) return found_devices[0] @command("device", [MainContext, DeviceContext]) -def handle_device(context: MainContext, args: List[str]): +def handle_device(context: MainContext, args: list[str]) -> None: """ Manage your devices """ @@ -47,7 +48,7 @@ def handle_device(context: MainContext, args: List[str]): @handle_device.subcommand("list") -def handle_device_list(context: MainContext, args: List[str]): +def handle_device_list(context: MainContext, args: list[str]) -> None: """ List your devices """ @@ -55,7 +56,7 @@ def handle_device_list(context: MainContext, args: List[str]): if len(args) != 0: raise CommandError("usage: device list") - devices: List[Device] = Device.list_devices(context.client) + devices: list[Device] = Device.list_devices(context.client) if not devices: print("You don't have any devices.") else: @@ -65,7 +66,7 @@ def handle_device_list(context: MainContext, args: List[str]): @handle_device.subcommand("create") -def handle_device_create(context: MainContext, args: List[str]): +def handle_device_create(context: MainContext, args: list[str]) -> None: """ Create your starter device """ @@ -75,7 +76,7 @@ def handle_device_create(context: MainContext, args: List[str]): try: device: Device = Device.starter_device(context.client) - except AlreadyOwnADeviceException: + except AlreadyOwnADeviceError: raise CommandError("You already own a device.") print("Your device has been created!") @@ -83,7 +84,7 @@ def handle_device_create(context: MainContext, args: List[str]): @handle_device.subcommand("build") -def handle_device_build(context: MainContext, args: List[str]): +def handle_device_build(context: MainContext, args: list[str]) -> None: """ Build a new device """ @@ -91,42 +92,42 @@ def handle_device_build(context: MainContext, args: List[str]): if len(args) < 5: raise CommandError("usage: device build [...] [...]") - hardware: dict = context.client.get_hardware_config() + hardware: HardwareConfig = context.client.get_hardware_config() mainboard, cpu, gpu, *ram_and_disk = args - ram: List[str] = [] - disk: List[str] = [] + ram: list[str] = [] + disk: list[str] = [] - for e in hardware["mainboards"]: + for e in hardware.mainboard: if e.replace(" ", "") == mainboard: - mainboard: str = e + mainboard = e break else: print(f"'{mainboard}' is no mainboard.") return - for e in hardware["cpu"]: + for e in hardware.cpu: if e.replace(" ", "") == cpu: - cpu: str = e + cpu = e break else: print(f"'{cpu}' is no cpu.") return - for e in hardware["gpu"]: + for e in hardware.gpu: if e.replace(" ", "") == gpu: - gpu: str = e + gpu = e break else: print(f"'{gpu}' is no gpu.") return for element in ram_and_disk: - for e in hardware["ram"]: + for e in hardware.ram: if e.replace(" ", "") == element: ram.append(e) break else: - for e in hardware["disk"]: + for e in hardware.disk: if e.replace(" ", "") == element: disk.append(e) break @@ -139,26 +140,26 @@ def handle_device_build(context: MainContext, args: List[str]): if not disk: raise CommandError("You have to chose at least one hard drive.") - inventory: List[str] = [e.name for e in InventoryElement.list_inventory(context.client)] - inventory_complete: bool = True + inventory: list[str] = [e.name for e in InventoryElement.list_inventory(context.client)] + inventory_complete = True for element in [mainboard, cpu, gpu] + ram + disk: if element in inventory: inventory.remove(element) else: print(f"'{element}' could not be found in your inventory.") - inventory_complete: bool = False + inventory_complete = False if not inventory_complete: return try: device: Device = Device.build(context.client, mainboard, cpu, gpu, ram, disk) - except IncompatibleCPUSocket: + except IncompatibleCPUSocketError: raise CommandError("The mainboard socket is not compatible with the cpu.") - except NotEnoughRAMSlots: + except NotEnoughRAMSlotsError: raise CommandError("The mainboard has not enough ram slots.") - except IncompatibleRAMTypes: + except IncompatibleRAMTypesError: raise CommandError("A ram type is incompatible with the mainboard.") - except IncompatibleDriverInterface: + except IncompatibleDriverInterfaceError: raise CommandError("The drive interface is not compatible with the mainboard.") else: print("Your device has been created!") @@ -166,7 +167,7 @@ def handle_device_build(context: MainContext, args: List[str]): @handle_device.subcommand("boot", aliases=["start"]) -def handle_device_boot(context: MainContext, args: List[str]): +def handle_device_boot(context: MainContext, args: list[str]) -> None: """ Boot a device """ @@ -182,7 +183,7 @@ def handle_device_boot(context: MainContext, args: List[str]): @handle_device.subcommand("shutdown", aliases=["poweroff", "halt"]) -def handle_device_shutdown(context: MainContext, args: List[str]): +def handle_device_shutdown(context: MainContext, args: list[str]) -> None: """ Shut down a device """ @@ -200,7 +201,7 @@ def handle_device_shutdown(context: MainContext, args: List[str]): @command("shutdown", [DeviceContext], ["poweroff", "halt"]) -def handle_shutdown(context: DeviceContext, _): +def handle_shutdown(context: DeviceContext, _: Any) -> None: """Shutdown this device""" device: Device = context.host @@ -212,7 +213,7 @@ def handle_shutdown(context: DeviceContext, _): @handle_device.subcommand("connect") -def handle_device_connect(context: MainContext, args: List[str]): +def handle_device_connect(context: MainContext, args: list[str]) -> None: """ Connect to one of your devices """ @@ -226,11 +227,11 @@ def handle_device_connect(context: MainContext, args: List[str]): return device.power() - context.open(DeviceContext(context.root_context, context.session_token, device)) + context.open(DeviceContext(context.root_context, cast(str, context.session_token), device)) @handle_device.subcommand("delete") -def handle_device_delete(context: MainContext, args: List[str]): +def handle_device_delete(context: MainContext, args: list[str]) -> None: """ Delete a device """ @@ -239,8 +240,14 @@ def handle_device_delete(context: MainContext, args: List[str]): raise CommandError("usage: device delete ") device: Device = get_device(context, args[0]) + if not context.confirm(f"Are you sure you want to delete the device '{device.name}' including all its files?"): + return + + try: + device.delete() + except DeviceIsStarterDeviceError: + raise CommandError("You cannot delete your starter device.") - device.delete() print("Device has been deleted.") @@ -248,31 +255,31 @@ def handle_device_delete(context: MainContext, args: List[str]): @handle_device_shutdown.completer() @handle_device_connect.completer() @handle_device_delete.completer() -def complete_device(context: MainContext, args: List[str]) -> List[str]: +def complete_device(context: MainContext, args: list[str]) -> list[str]: if len(args) == 1: - device_names: List[str] = [device.name for device in Device.list_devices(context.client)] + device_names: list[str] = [device.name for device in Device.list_devices(context.client)] return [name for name in device_names if device_names.count(name) == 1] return [] @handle_device_build.completer() -def complete_build(context: MainContext, args: List[str]) -> List[str]: +def complete_build(context: MainContext, args: list[str]) -> list[str]: if len(args) == 1: - return [name.replace(" ", "") for name in list(context.client.get_hardware_config()["mainboards"])] + return [name.replace(" ", "") for name in list(context.client.get_hardware_config().mainboard)] if len(args) == 2: - return [name.replace(" ", "") for name in list(context.client.get_hardware_config()["cpu"])] + return [name.replace(" ", "") for name in list(context.client.get_hardware_config().cpu)] if len(args) == 3: - return [name.replace(" ", "") for name in list(context.client.get_hardware_config()["gpu"])] + return [name.replace(" ", "") for name in list(context.client.get_hardware_config().gpu)] if len(args) == 4: - return [name.replace(" ", "") for name in list(context.client.get_hardware_config()["ram"])] + return [name.replace(" ", "") for name in list(context.client.get_hardware_config().ram)] if len(args) >= 5: - hardware: dict = context.client.get_hardware_config() - return [name.replace(" ", "") for name in list(hardware["ram"]) + list(hardware["disk"])] + hardware: HardwareConfig = context.client.get_hardware_config() + return [name.replace(" ", "") for name in list(hardware.ram) + list(hardware.disk)] return [] @command("hostname", [DeviceContext]) -def handle_hostname(context: DeviceContext, args: List[str]): +def handle_hostname(context: DeviceContext, args: list[str]) -> None: """ Show or modify the name of the device """ @@ -289,7 +296,7 @@ def handle_hostname(context: DeviceContext, args: List[str]): @command("top", [DeviceContext]) -def handle_top(context: DeviceContext, *_): +def handle_top(context: DeviceContext, _: Any) -> None: """ Display the current resource usage of this device """ @@ -297,7 +304,7 @@ def handle_top(context: DeviceContext, *_): print(f"Resource usage of '{context.host.name}':") print() resource_usage: ResourceUsage = context.host.get_resource_usage() - hardware: Dict[str, DeviceHardware] = {dh.hardware_type: dh for dh in context.host.get_hardware()} + hardware: dict[str, DeviceHardware] = {dh.hardware_type: dh for dh in context.host.get_hardware()} print(f" Mainboard: {hardware['mainboard'].hardware_element}") print() diff --git a/PyCrypCli/commands/files.py b/PyCrypCli/commands/files.py index 5c5bb75..b3c8738 100644 --- a/PyCrypCli/commands/files.py +++ b/PyCrypCli/commands/files.py @@ -1,43 +1,39 @@ -from typing import List, Optional, Tuple - -from PyCrypCli.exceptions import ( - FileAlreadyExistsException, - InvalidWalletFile, - UnknownSourceOrDestinationException, - PermissionDeniedException, - FileNotChangeableException, +from typing import Any, cast + +from .command import command, CommandError +from ..context import DeviceContext +from ..exceptions import ( + FileAlreadyExistsError, + InvalidWalletFileError, + UnknownSourceOrDestinationError, + PermissionDeniedError, + FileNotChangeableError, ) - -from PyCrypCli.commands import command, CommandError -from PyCrypCli.context import DeviceContext -from PyCrypCli.game_objects import File, Wallet +from ..models import File, Wallet @command("ls", [DeviceContext], aliases=["l", "dir"]) -def handle_ls(context: DeviceContext, args: List[str]): +def handle_ls(context: DeviceContext, args: list[str]) -> None: """ List all files """ - if not args: - directory: File = context.pwd - else: - directory: File = context.path_to_file(args[0]) - if directory is None: - raise CommandError("No such file or directory.") + directory = context.pwd if not args else context.path_to_file(args[0]) + if directory is None: + raise CommandError("No such file or directory.") if directory.is_directory: - files: List[File] = context.get_files(directory.uuid) - files.sort(key=lambda f: [1 - f.is_directory, f.filename]) + files: list[File] = context.get_files(directory.uuid) + files.sort(key=lambda f: [1 - f.is_directory, f.name]) else: - files: List[File] = [directory] + files = [directory] for file in files: - print(["[FILE] ", "[DIR] "][file.is_directory] + file.filename) + print(["[FILE] ", "[DIR] "][file.is_directory] + file.name) @command("pwd", [DeviceContext]) -def handle_pwd(context: DeviceContext, *_): +def handle_pwd(context: DeviceContext, _: Any) -> None: """ Print the current working directory """ @@ -46,7 +42,7 @@ def handle_pwd(context: DeviceContext, *_): @command("mkdir", [DeviceContext]) -def handle_mkdir(context: DeviceContext, args: List[str]): +def handle_mkdir(context: DeviceContext, args: list[str]) -> None: """ Create a new directory """ @@ -55,7 +51,7 @@ def handle_mkdir(context: DeviceContext, args: List[str]): raise CommandError("usage: mkdir ") *path, dirname = args[0].split("/") - parent: Optional[File] = context.path_to_file("/".join(path)) + parent: File | None = context.path_to_file("/".join(path)) if parent is None: raise CommandError("No such file or directory.") if not parent.is_directory: @@ -63,12 +59,12 @@ def handle_mkdir(context: DeviceContext, args: List[str]): try: context.host.create_file(dirname, "", True, parent.uuid) - except FileAlreadyExistsException: + except FileAlreadyExistsError: raise CommandError("There already exists a file with this name.") @command("cd", [DeviceContext]) -def handle_cd(context: DeviceContext, args: List[str]): +def handle_cd(context: DeviceContext, args: list[str]) -> None: """ Change the current working directory """ @@ -76,7 +72,7 @@ def handle_cd(context: DeviceContext, args: List[str]): if not args: context.pwd = context.get_root_dir() else: - directory: Optional[File] = context.path_to_file(args[0]) + directory: File | None = context.path_to_file(args[0]) if directory is None: raise CommandError("The specified directory does not exist") if not directory.is_directory: @@ -86,7 +82,7 @@ def handle_cd(context: DeviceContext, args: List[str]): @command("..", [DeviceContext]) -def handle_dot_dot(context: DeviceContext, _): +def handle_dot_dot(context: DeviceContext, _: Any) -> None: """ Go to parent directory """ @@ -94,16 +90,18 @@ def handle_dot_dot(context: DeviceContext, _): handle_cd(context, [".."]) -def create_file(context: DeviceContext, filepath: str, content: str): +def create_file(context: DeviceContext, filepath: str, content: str) -> None: *path, filename = filepath.split("/") - parent: Optional[File] = context.path_to_file("/".join(path)) + parent: File | None = context.path_to_file("/".join(path)) + if not parent: + raise CommandError("Parent directory does not exist.") if not filename: raise CommandError("Filename cannot be empty.") if len(filename) > 64: raise CommandError("Filename cannot be longer than 64 characters.") - file: File = context.get_file(filename, parent.uuid) + file: File | None = context.get_file(filename, parent.uuid) if file is not None: if file.is_directory: raise CommandError("A directory with this name already exists.") @@ -113,7 +111,7 @@ def create_file(context: DeviceContext, filepath: str, content: str): @command("touch", [DeviceContext]) -def handle_touch(context: DeviceContext, args: List[str]): +def handle_touch(context: DeviceContext, args: list[str]) -> None: """ Create a new file with given content """ @@ -126,7 +124,7 @@ def handle_touch(context: DeviceContext, args: List[str]): @command("cat", [DeviceContext]) -def handle_cat(context: DeviceContext, args: List[str]): +def handle_cat(context: DeviceContext, args: list[str]) -> None: """ Print the content of a file """ @@ -135,7 +133,7 @@ def handle_cat(context: DeviceContext, args: List[str]): raise CommandError("usage: cat ") path: str = args[0] - file: File = context.path_to_file(path) + file: File | None = context.path_to_file(path) if file is None: raise CommandError("File does not exist.") if file.is_directory: @@ -145,7 +143,7 @@ def handle_cat(context: DeviceContext, args: List[str]): @command("rm", [DeviceContext]) -def handle_rm(context: DeviceContext, args: List[str]): +def handle_rm(context: DeviceContext, args: list[str]) -> None: """ Remove a file """ @@ -154,7 +152,7 @@ def handle_rm(context: DeviceContext, args: List[str]): raise CommandError("usage: rm ") filepath: str = args[0] - file: File = context.path_to_file(filepath) + file: File | None = context.path_to_file(filepath) if file is None: raise CommandError("File does not exist.") @@ -167,9 +165,9 @@ def handle_rm(context: DeviceContext, args: List[str]): break pwd = context.get_parent_dir(pwd) - question: str = f"Are you sure you want to delete the directory `{filepath}` including all contained files?" + question: str = f"Are you sure you want to delete the directory '{filepath}' including all contained files?" else: - question: str = f"Are you sure you want to delete this file `{filepath}`?" + question = f"Are you sure you want to delete the file '{filepath}'?" if context.ask(question + " [yes|no] ", ["yes", "no"]) == "no": raise CommandError("File has not been deleted.") @@ -187,29 +185,27 @@ def handle_rm(context: DeviceContext, args: List[str]): else: print("The following key might now be the only way to access your wallet.") print(content) - except (InvalidWalletFile, UnknownSourceOrDestinationException, PermissionDeniedException): + except (InvalidWalletFileError, UnknownSourceOrDestinationError, PermissionDeniedError): pass try: file.delete() - except FileNotChangeableException: + except FileNotChangeableError: raise CommandError("Some files could not be deleted.") def check_file_movable( - context: DeviceContext, - source: str, - destination: str, - move: bool, -) -> Optional[Tuple[File, str, str]]: - file: Optional[File] = context.path_to_file(source) + context: DeviceContext, source: str, destination: str, move: bool +) -> tuple[File, str, str | None] | None: + file: File | None = context.path_to_file(source) if file is None: raise CommandError("File does not exist.") - dest_file: Optional[File] = context.path_to_file(destination) + dest_file: File | None = context.path_to_file(destination) absolute = destination[0] == "/" dest_parent_path, _, dest_name = destination[absolute:].rpartition("/") - dest_parent: Optional[File] = context.path_to_file("/" * absolute + dest_parent_path) + dest_parent: File | None = context.path_to_file("/" * absolute + dest_parent_path) + dest_dir: str | None if file.is_directory: if dest_file is None: @@ -217,9 +213,9 @@ def check_file_movable( raise CommandError("No such file or directory.") if not dest_parent.is_directory: raise CommandError("Not a directory.") - dest_dir: str = dest_parent.uuid + dest_dir = dest_parent.uuid elif dest_file.is_directory: - sub_file: Optional[File] = context.get_file(dest_name, dest_file.uuid) + sub_file: File | None = context.get_file(dest_name, dest_file.uuid) if sub_file is not None: if sub_file.is_directory: if context.get_files(sub_file.uuid): @@ -227,8 +223,8 @@ def check_file_movable( sub_file.delete() else: raise CommandError("Directory cannot replace a file.") - dest_name: str = file.filename - dest_dir: str = dest_file.uuid + dest_name = file.name + dest_dir = dest_file.uuid else: raise CommandError("Directory cannot replace a file.") else: @@ -237,29 +233,29 @@ def check_file_movable( raise CommandError("No such file or directory.") if not dest_parent.is_directory: raise CommandError("Not a directory.") - dest_dir: str = dest_parent.uuid + dest_dir = dest_parent.uuid elif dest_file.is_directory: - sub_file: Optional[File] = context.get_file(dest_name, dest_file.uuid) + sub_file = context.get_file(dest_name, dest_file.uuid) if sub_file is not None: if sub_file.is_directory: raise CommandError("File cannot replace a directory.") sub_file.delete() - dest_name: str = file.filename - dest_dir: str = dest_file.uuid + dest_name = file.name + dest_dir = dest_file.uuid else: dest_file.delete() - dest_dir: str = dest_parent.uuid + dest_dir = cast(File, dest_parent).uuid - if dest_dir == file.parent_dir_uuid and dest_name == file.filename: + if dest_dir == file.parent_dir_uuid and dest_name == file.name: return None if dest_dir is not None: - dir_to_check: File = File.get_file(context.client, file.device, dest_dir) + dir_to_check: File | None = File.get_file(context.client, file.device_uuid, dest_dir) while True: + if not dir_to_check: + break if dir_to_check.uuid == file.uuid: raise CommandError(f"You cannot {['copy', 'move'][move]} a directory into itself.") - if dir_to_check.uuid is None: - break dir_to_check = context.get_parent_dir(dir_to_check) if not dest_name: @@ -271,7 +267,7 @@ def check_file_movable( @command("cp", [DeviceContext]) -def handle_cp(context: DeviceContext, args: List[str]): +def handle_cp(context: DeviceContext, args: list[str]) -> None: """ Create a copy of a file """ @@ -279,22 +275,22 @@ def handle_cp(context: DeviceContext, args: List[str]): if len(args) != 2: raise CommandError("usage: cp ") - result: Optional[Tuple[File, str, str]] = check_file_movable(context, args[0], args[1], move=False) + result: tuple[File, str, str | None] | None = check_file_movable(context, args[0], args[1], move=False) if result is None: return file, dest_name, dest_dir = result - queue: List[Tuple[File, str, str]] = [(file, dest_name, dest_dir)] + queue: list[tuple[File, str, str | None]] = [(file, dest_name, dest_dir)] while queue: file, dest_name, dest_dir = queue.pop(0) new_file: File = context.host.create_file(dest_name, file.content, file.is_directory, dest_dir) if file.is_directory: for child in context.get_files(file.uuid): - queue.append((child, child.filename, new_file.uuid)) + queue.append((child, child.name, cast(str, new_file.uuid))) @command("mv", [DeviceContext]) -def handle_mv(context: DeviceContext, args: List[str]): +def handle_mv(context: DeviceContext, args: list[str]) -> None: """ Rename a file """ @@ -302,7 +298,7 @@ def handle_mv(context: DeviceContext, args: List[str]): if len(args) != 2: raise CommandError("usage: mv ") - result: Optional[Tuple[File, str, str]] = check_file_movable(context, args[0], args[1], move=True) + result: tuple[File, str, str | None] | None = check_file_movable(context, args[0], args[1], move=True) if result is None: return @@ -314,7 +310,7 @@ def handle_mv(context: DeviceContext, args: List[str]): @handle_cat.completer() @handle_touch.completer() @handle_rm.completer() -def simple_file_completer(context: DeviceContext, args: List[str]) -> List[str]: +def simple_file_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return context.file_path_completer(args[0]) return [] @@ -322,7 +318,7 @@ def simple_file_completer(context: DeviceContext, args: List[str]) -> List[str]: @handle_cd.completer() @handle_mkdir.completer() -def simple_directory_completer(context: DeviceContext, args: List[str]) -> List[str]: +def simple_directory_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return context.file_path_completer(args[0], dirs_only=True) return [] @@ -330,7 +326,7 @@ def simple_directory_completer(context: DeviceContext, args: List[str]) -> List[ @handle_mv.completer() @handle_cp.completer() -def copy_completer(context: DeviceContext, args: List[str]) -> List[str]: +def copy_completer(context: DeviceContext, args: list[str]) -> list[str]: if 1 <= len(args) <= 2: return context.file_path_completer(args[-1]) return [] diff --git a/PyCrypCli/commands/help.py b/PyCrypCli/commands/help.py index 41f9cec..4a901a4 100644 --- a/PyCrypCli/commands/help.py +++ b/PyCrypCli/commands/help.py @@ -1,16 +1,14 @@ -from typing import List, Optional, Dict, Tuple +from .command import command, Command, CommandError +from ..context import LoginContext, MainContext, DeviceContext, Context -from PyCrypCli.commands import command, Command, CommandError -from PyCrypCli.context import LoginContext, MainContext, DeviceContext, Context - -def print_help(context: Context, cmd: Optional[Command]): +def print_help(context: Context, cmd: Command | None) -> None: if cmd is None: - cmds: Dict[str, Command] = {c.name: c for c in context.get_commands().values()} + cmds: dict[str, Command] = {c.name: c for c in context.get_commands().values()} else: print(cmd.description) - cmds: Dict[str, Command] = {c.name: c for c in cmd.prepared_subcommands.get(type(context), {}).values()} - command_list: List[Tuple[str, str]] = [ + cmds = {c.name: c for c in cmd.prepared_subcommands.get(type(context), {}).values()} + command_list: list[tuple[str, str]] = [ ("|".join([name] + cmd.aliases), cmd.description) for name, cmd in cmds.items() ] if not command_list: @@ -23,12 +21,12 @@ def print_help(context: Context, cmd: Optional[Command]): print(f"Available {'sub' * (cmd is not None)}commands:") max_length: int = max([len(cmd[0]) for cmd in command_list]) for com, desc in command_list: - com: str = com.ljust(max_length) + com = com.ljust(max_length) print(f" - {com} {desc}") @command("help", [LoginContext, MainContext, DeviceContext]) -def handle_main_help(context: Context, args: List[str]): +def handle_main_help(context: Context, args: list[str]) -> None: """ Show a list of available commands """ @@ -46,7 +44,7 @@ def handle_main_help(context: Context, args: List[str]): @handle_main_help.completer() -def complete_help(context: Context, args: List[str]) -> List[str]: +def complete_help(context: Context, args: list[str]) -> list[str]: if len(args) == 1: return list(context.get_commands()) @@ -54,3 +52,5 @@ def complete_help(context: Context, args: List[str]) -> List[str]: cmd, args = context.get_commands()[args[0]].parse_command(context, args[1:-1]) if not args: return list(cmd.prepared_subcommands.get(type(context), {})) + + return [] diff --git a/PyCrypCli/commands/inventory.py b/PyCrypCli/commands/inventory.py index 65a75e5..f2bc333 100644 --- a/PyCrypCli/commands/inventory.py +++ b/PyCrypCli/commands/inventory.py @@ -1,16 +1,16 @@ from collections import Counter -from typing import List, Dict +from typing import List, Dict, Any -from PyCrypCli.commands import CommandError, command -from PyCrypCli.commands.help import print_help -from PyCrypCli.context import MainContext, DeviceContext -from PyCrypCli.exceptions import CannotTradeWithYourselfException, UserUUIDDoesNotExistException -from PyCrypCli.game_objects import InventoryElement, ShopCategory -from PyCrypCli.util import print_tree +from .command import CommandError, command +from .help import print_help +from ..context import MainContext, DeviceContext +from ..exceptions import CannotTradeWithYourselfError, UserUUIDDoesNotExistError +from ..models import InventoryElement, ShopCategory +from ..util import print_tree @command("inventory", [MainContext, DeviceContext]) -def handle_inventory(context: MainContext, args: List[str]): +def handle_inventory(context: MainContext, args: List[str]) -> None: """ Manage your inventory and trade with other players """ @@ -21,7 +21,7 @@ def handle_inventory(context: MainContext, args: List[str]): @handle_inventory.subcommand("list") -def handle_inventory_list(context: MainContext, _): +def handle_inventory_list(context: MainContext, _: Any) -> None: """ List your inventory """ @@ -35,7 +35,7 @@ def handle_inventory_list(context: MainContext, _): for category in categories: category_tree = [] for subcategory in category.subcategories: - subcategory_tree = [ + subcategory_tree: list[tuple[str, list[Any]]] = [ (f"{inventory[item.name]}x {item.name}", []) for item in subcategory.items if inventory[item.name] ] if subcategory_tree: @@ -53,7 +53,7 @@ def handle_inventory_list(context: MainContext, _): @handle_inventory.subcommand("trade") -def handle_inventory_trade(context: MainContext, args: List[str]): +def handle_inventory_trade(context: MainContext, args: List[str]) -> None: """ Trade with other players """ @@ -71,9 +71,9 @@ def handle_inventory_trade(context: MainContext, args: List[str]): try: item.trade(target_user) - except CannotTradeWithYourselfException: + except CannotTradeWithYourselfError: raise CommandError("You cannot trade with yourself.") - except UserUUIDDoesNotExistException: + except UserUUIDDoesNotExistError: raise CommandError("This user does not exist.") diff --git a/PyCrypCli/commands/miner.py b/PyCrypCli/commands/miner.py index e981d4a..33d348c 100644 --- a/PyCrypCli/commands/miner.py +++ b/PyCrypCli/commands/miner.py @@ -1,23 +1,22 @@ -from typing import List +from typing import Any -from PyCrypCli.exceptions import ServiceNotFoundException, WalletNotFoundException - -from PyCrypCli.commands import CommandError, command -from PyCrypCli.commands.help import print_help -from PyCrypCli.context import DeviceContext -from PyCrypCli.game_objects import Miner -from PyCrypCli.util import is_uuid +from .command import CommandError, command +from .help import print_help +from ..context import DeviceContext +from ..exceptions import ServiceNotFoundError, WalletNotFoundError +from ..models import Miner +from ..util import is_uuid def get_miner(context: DeviceContext) -> Miner: try: return context.host.get_miner() - except ServiceNotFoundException: + except ServiceNotFoundError: raise CommandError("You have to create the miner service before you can use it.") @command("miner", [DeviceContext]) -def handle_miner(context: DeviceContext, args: List[str]): +def handle_miner(context: DeviceContext, args: list[str]) -> None: """ Manager your Morphcoin miners """ @@ -28,13 +27,13 @@ def handle_miner(context: DeviceContext, args: List[str]): @handle_miner.subcommand("look") -def handle_miner_look(context: DeviceContext, _): +def handle_miner_look(context: DeviceContext, _: Any) -> None: """ View miner configuration """ miner: Miner = get_miner(context) - print("Destination wallet: " + miner.wallet) + print("Destination wallet: " + miner.wallet_uuid) print("Running: " + ["no", "yes"][miner.running]) print(f"Power: {miner.power * 100}%") if miner.running: @@ -42,7 +41,7 @@ def handle_miner_look(context: DeviceContext, _): @handle_miner.subcommand("power") -def handle_miner_power(context: DeviceContext, args: List[str]): +def handle_miner_power(context: DeviceContext, args: list[str]) -> None: """ Change miner power """ @@ -61,12 +60,12 @@ def handle_miner_power(context: DeviceContext, args: List[str]): try: miner.set_power(power) - except WalletNotFoundException: + except WalletNotFoundError: raise CommandError("Wallet does not exist.") @handle_miner.subcommand("wallet") -def handle_miner_wallet(context: DeviceContext, args: List[str]): +def handle_miner_wallet(context: DeviceContext, args: list[str]) -> None: """ Connect the miner to a different wallet """ @@ -81,5 +80,5 @@ def handle_miner_wallet(context: DeviceContext, args: List[str]): try: miner.set_wallet(args[0]) - except WalletNotFoundException: + except WalletNotFoundError: raise CommandError("Wallet does not exist.") diff --git a/PyCrypCli/commands/morphcoin.py b/PyCrypCli/commands/morphcoin.py index f09da9b..4d5ddbd 100644 --- a/PyCrypCli/commands/morphcoin.py +++ b/PyCrypCli/commands/morphcoin.py @@ -1,42 +1,42 @@ import re import time -from typing import List - -from PyCrypCli.commands import command, CommandError -from PyCrypCli.commands.files import create_file -from PyCrypCli.commands.help import print_help -from PyCrypCli.context import DeviceContext -from PyCrypCli.exceptions import ( - FileNotFoundException, - InvalidWalletFile, - UnknownSourceOrDestinationException, - PermissionDeniedException, - AlreadyOwnAWalletException, +from typing import Any + +from .command import command, CommandError +from .files import create_file +from .help import print_help +from ..context import DeviceContext +from ..exceptions import ( + FileNotFoundError, + InvalidWalletFileError, + UnknownSourceOrDestinationError, + PermissionDeniedError, + AlreadyOwnAWalletError, ) -from PyCrypCli.game_objects import Wallet, Transaction, PublicWallet -from PyCrypCli.util import is_uuid, extract_wallet, strip_float +from ..models import Wallet, Transaction, PublicWallet +from ..util import is_uuid, extract_wallet, strip_float def get_wallet_from_file(context: DeviceContext, path: str) -> Wallet: try: return get_wallet(context, *context.get_wallet_credentials_from_file(path)) - except FileNotFoundException: + except FileNotFoundError: raise CommandError("File does not exist.") - except InvalidWalletFile: + except InvalidWalletFileError: raise CommandError("File is no wallet file.") def get_wallet(context: DeviceContext, uuid: str, key: str) -> Wallet: try: return Wallet.get_wallet(context.client, uuid, key) - except UnknownSourceOrDestinationException: + except UnknownSourceOrDestinationError: raise CommandError("Invalid wallet file. Wallet does not exist.") - except PermissionDeniedException: + except PermissionDeniedError: raise CommandError("Invalid wallet file. Key is incorrect.") @command("morphcoin", [DeviceContext]) -def handle_morphcoin(context: DeviceContext, args: List[str]): +def handle_morphcoin(context: DeviceContext, args: list[str]) -> None: """ Manage your Morphcoin wallet """ @@ -47,7 +47,7 @@ def handle_morphcoin(context: DeviceContext, args: List[str]): @handle_morphcoin.subcommand("create") -def handle_morphcoin_create(context: DeviceContext, args: List[str]): +def handle_morphcoin_create(context: DeviceContext, args: list[str]) -> None: """ Create a new MorphCoin wallet """ @@ -66,17 +66,17 @@ def handle_morphcoin_create(context: DeviceContext, args: List[str]): except CommandError: wallet.delete() raise - except AlreadyOwnAWalletException: + except AlreadyOwnAWalletError: raise CommandError("You already own a wallet") @handle_morphcoin.subcommand("list") -def handle_morphcoin_list(context: DeviceContext, _): +def handle_morphcoin_list(context: DeviceContext, _: Any) -> None: """ List your MorphCoin wallets """ - wallets: List[PublicWallet] = PublicWallet.list_wallets(context.client) + wallets: list[PublicWallet] = PublicWallet.list_wallets(context.client) if not wallets: print("You don't own any wallet.") else: @@ -86,7 +86,7 @@ def handle_morphcoin_list(context: DeviceContext, _): @handle_morphcoin.subcommand("look") -def handle_morphcoin_look(context: DeviceContext, args: List[str]): +def handle_morphcoin_look(context: DeviceContext, args: list[str]) -> None: """ View the balance of your wallet """ @@ -105,7 +105,7 @@ def handle_morphcoin_look(context: DeviceContext, args: List[str]): @handle_morphcoin.subcommand("transactions") -def handle_morphcoin_transactions(context: DeviceContext, args: List[str]): +def handle_morphcoin_transactions(context: DeviceContext, args: list[str]) -> None: """ View the transaction history of your wallet """ @@ -115,19 +115,19 @@ def handle_morphcoin_transactions(context: DeviceContext, args: List[str]): wallet: Wallet = get_wallet_from_file(context, args[0]) - if not wallet.transactions: + if not wallet.transaction_count: print("No transactions found for this wallet.") return - transactions: List[Transaction] = wallet.get_transactions(wallet.transactions, 0) + transactions: list[Transaction] = wallet.get_transactions(wallet.transaction_count, 0) print("Transactions for this wallet:") for transaction in transactions: source: str = transaction.source_uuid if source == wallet.uuid: - source: str = "self" + source = "self" destination: str = transaction.destination_uuid if destination == wallet.uuid: - destination: str = "self" + destination = "self" amount: int = transaction.amount usage: str = transaction.usage text = f"{transaction.timestamp.ctime()}| {strip_float(amount / 1000, 3)} MC: {source} -> {destination}" @@ -137,7 +137,7 @@ def handle_morphcoin_transactions(context: DeviceContext, args: List[str]): @handle_morphcoin.subcommand("reset") -def handle_morphcoin_reset(context: DeviceContext, args: List[str]): +def handle_morphcoin_reset(context: DeviceContext, args: list[str]) -> None: """ Reset your wallet in case you lost the secret key """ @@ -151,14 +151,14 @@ def handle_morphcoin_reset(context: DeviceContext, args: List[str]): try: PublicWallet.get_public_wallet(context.client, wallet_uuid).reset_wallet() - except UnknownSourceOrDestinationException: + except UnknownSourceOrDestinationError: raise CommandError("Wallet does not exist.") - except PermissionDeniedException: + except PermissionDeniedError: raise CommandError("Permission denied.") @handle_morphcoin.subcommand("watch") -def handle_morphcoin_watch(context: DeviceContext, args: List[str]): +def handle_morphcoin_watch(context: DeviceContext, args: list[str]) -> None: """ Live view of the wallet balance """ @@ -176,9 +176,9 @@ def handle_morphcoin_watch(context: DeviceContext, args: List[str]): now = time.time() if now - last_update > 20: - wallet: Wallet = get_wallet(context, wallet.uuid, wallet.key) - current_mining_rate: float = wallet.get_mining_rate() - last_update: float = now + wallet = get_wallet(context, wallet.uuid, wallet.key) + current_mining_rate = wallet.get_mining_rate() + last_update = now current_balance: int = wallet.amount + int(current_mining_rate * 1000 * (now - last_update)) print( @@ -193,35 +193,35 @@ def handle_morphcoin_watch(context: DeviceContext, args: List[str]): @handle_morphcoin_look.completer() @handle_morphcoin_transactions.completer() @handle_morphcoin_watch.completer() -def morphcoin_completer(context: DeviceContext, args: List[str]) -> List[str]: +def morphcoin_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return context.file_path_completer(args[0]) return [] @handle_morphcoin_create.completer() -def morphcoin_create_completer(context: DeviceContext, args: List[str]) -> List[str]: +def morphcoin_create_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return context.file_path_completer(args[0], dirs_only=True) return [] @command("pay", [DeviceContext]) -def handle_pay(context: DeviceContext, args: List[str]): +def handle_pay(context: DeviceContext, args: list[str]) -> None: """ Send Morphcoins to another wallet """ if len(args) < 3: raise CommandError( - "usage: pay [usage]\n" " or: pay [usage]", + "usage: pay [usage]\n" " or: pay [usage]" ) if extract_wallet(f"{args[0]} {args[1]}") is not None: wallet: Wallet = get_wallet(context, args[0], args[1]) args.pop(0) else: - wallet: Wallet = get_wallet_from_file(context, args[0]) + wallet = get_wallet_from_file(context, args[0]) receiver: str = args[1] if not is_uuid(receiver): @@ -239,12 +239,12 @@ def handle_pay(context: DeviceContext, args: List[str]): try: wallet.send(PublicWallet.get_public_wallet(context.client, receiver), amount, " ".join(args[3:])) print(f"Sent {strip_float(amount / 1000, 3)} morphcoin to {receiver}.") - except UnknownSourceOrDestinationException: + except UnknownSourceOrDestinationError: raise CommandError("Destination wallet does not exist.") @handle_pay.completer() -def pay_completer(context: DeviceContext, args: List[str]) -> List[str]: +def pay_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return context.file_path_completer(args[0]) return [] diff --git a/PyCrypCli/commands/network.py b/PyCrypCli/commands/network.py index 2b8df48..425b11b 100644 --- a/PyCrypCli/commands/network.py +++ b/PyCrypCli/commands/network.py @@ -1,39 +1,39 @@ -from typing import List - -from PyCrypCli.commands import command, CommandError -from PyCrypCli.commands.device import get_device -from PyCrypCli.commands.help import print_help -from PyCrypCli.context import DeviceContext -from PyCrypCli.exceptions import ( - MaximumNetworksReachedException, - InvalidNameException, - NameAlreadyInUseException, - NetworkNotFoundException, - AlreadyMemberOfNetworkException, - InvitationAlreadyExistsException, - NoPermissionsException, - CannotLeaveOwnNetworkException, - CannotKickOwnerException, - DeviceNotFoundException, +from typing import Any + +from .command import command, CommandError +from .device import get_device +from .help import print_help +from ..context import DeviceContext +from ..exceptions import ( + MaximumNetworksReachedError, + InvalidNameError, + NameAlreadyInUseError, + NetworkNotFoundError, + AlreadyMemberOfNetworkError, + InvitationAlreadyExistsError, + NoPermissionsError, + CannotLeaveOwnNetworkError, + CannotKickOwnerError, + DeviceNotFoundError, ) -from PyCrypCli.game_objects import Network, NetworkMembership, Device, NetworkInvitation -from PyCrypCli.util import is_uuid +from ..models import Network, NetworkMembership, Device, NetworkInvitation +from ..util import is_uuid def get_network(context: DeviceContext, name_or_uuid: str) -> Network: if is_uuid(name_or_uuid): try: return Network.get_by_uuid(context.client, name_or_uuid) - except NetworkNotFoundException: + except NetworkNotFoundError: pass try: return Network.get_network_by_name(context.client, name_or_uuid) - except NetworkNotFoundException: + except NetworkNotFoundError: raise CommandError("This network does not exist.") -def handle_membership_request(context: DeviceContext, args: List[str], accept: bool): +def handle_membership_request(context: DeviceContext, args: list[str], accept: bool) -> None: if len(args) not in (1, 2): raise CommandError(f"usage: network {['deny', 'accept'][accept]} []") @@ -41,26 +41,26 @@ def handle_membership_request(context: DeviceContext, args: List[str], accept: b if len(args) == 1: for invitation in context.host.get_network_invitations(): - if invitation.network == network.uuid: + if invitation.network_uuid == network.uuid: break else: raise CommandError("Invitation not found.") else: - devices: List[Device] = [] + devices: list[Device] = [] for request in network.get_membership_requests(): try: - devices.append(Device.get_device(context.client, request.device)) - except DeviceNotFoundException: + devices.append(Device.get_device(context.client, request.device_uuid)) + except DeviceNotFoundError: pass device: Device = get_device(context, args[1], devices) try: for invitation in network.get_membership_requests(): - if invitation.network == network.uuid and invitation.device == device.uuid: + if invitation.network_uuid == network.uuid and invitation.device_uuid == device.uuid: break else: raise CommandError("Join request not found.") - except NoPermissionsException: + except NoPermissionsError: raise CommandError("Permission denied.") if accept: @@ -70,7 +70,7 @@ def handle_membership_request(context: DeviceContext, args: List[str], accept: b @command("network", [DeviceContext]) -def handle_network(context: DeviceContext, args: List[str]): +def handle_network(context: DeviceContext, args: list[str]) -> None: """ Manage your networks """ @@ -81,41 +81,41 @@ def handle_network(context: DeviceContext, args: List[str]): @handle_network.subcommand("list") -def handle_network_list(context: DeviceContext, _): +def handle_network_list(context: DeviceContext, _: Any) -> None: """ View the networks this device is a member of """ - networks: List[Network] = context.host.get_networks() + networks: list[Network] = context.host.get_networks() if not networks: print("This device is not a member of any network.") else: print("Networks this device is a member of:") for network in networks: - owner: str = " (owner)" * (network.owner == context.host.uuid) + owner: str = " (owner)" * (network.owner_uuid == context.host.uuid) print(f" - [{['public', 'private'][network.hidden]}] {network.name}{owner} (UUID: {network.uuid})") @handle_network.subcommand("public") -def handle_network_public(context: DeviceContext, _): +def handle_network_public(context: DeviceContext, _: Any) -> None: """ View public networks """ - networks: List[Network] = Network.get_public_networks(context.client) + networks: list[Network] = Network.get_public_networks(context.client) if not networks: print("There is no public network.") else: print("Public networks:") for network in networks: - owner: str = " (owner)" * (network.owner == context.host.uuid) + owner: str = " (owner)" * (network.owner_uuid == context.host.uuid) print(f" - {network.name}{owner} (UUID: {network.uuid})") @handle_network.subcommand("create") -def handle_network_create(context: DeviceContext, args: List[str]): +def handle_network_create(context: DeviceContext, args: list[str]) -> None: """ Create a new network """ @@ -125,16 +125,16 @@ def handle_network_create(context: DeviceContext, args: List[str]): try: context.host.create_network(args[0], args[1] == "private") - except MaximumNetworksReachedException: + except MaximumNetworksReachedError: raise CommandError("You already own two networks.") - except InvalidNameException: + except InvalidNameError: raise CommandError("Invalid name.") - except NameAlreadyInUseException: + except NameAlreadyInUseError: raise CommandError("This name is already in use.") @handle_network.subcommand("members") -def handle_network_members(context: DeviceContext, args: List[str]): +def handle_network_members(context: DeviceContext, args: list[str]) -> None: """ View the members of one of your networks """ @@ -145,8 +145,8 @@ def handle_network_members(context: DeviceContext, args: List[str]): network: Network = get_network(context, args[0]) try: - members: List[NetworkMembership] = network.get_members() - except NetworkNotFoundException: + members: list[NetworkMembership] = network.get_members() + except NetworkNotFoundError: raise CommandError("Permission denied.") if not members: @@ -154,12 +154,12 @@ def handle_network_members(context: DeviceContext, args: List[str]): else: print(f"Members of '{network.name}':") for member in members: - device: Device = Device.get_device(context.client, member.device) + device: Device = Device.get_device(context.client, member.device_uuid) print(f" - [{['off', 'on'][device.powered_on]}] {device.name} (UUID: {device.uuid})") @handle_network.subcommand("request") -def handle_network_request(context: DeviceContext, args: List[str]): +def handle_network_request(context: DeviceContext, args: list[str]) -> None: """ Request membership of a network """ @@ -171,14 +171,14 @@ def handle_network_request(context: DeviceContext, args: List[str]): try: network.request_membership(context.host) - except AlreadyMemberOfNetworkException: + except AlreadyMemberOfNetworkError: raise CommandError("This device is already a member of the network.") - except InvitationAlreadyExistsException: + except InvitationAlreadyExistsError: raise CommandError("You already requested to join this network.") @handle_network.subcommand("requests") -def handle_network_requests(context: DeviceContext, args: List[str]): +def handle_network_requests(context: DeviceContext, args: list[str]) -> None: """ View open membership requests of one of your networks """ @@ -189,8 +189,8 @@ def handle_network_requests(context: DeviceContext, args: List[str]): network: Network = get_network(context, args[0]) try: - requests: List[NetworkInvitation] = network.get_membership_requests() - except NoPermissionsException: + requests: list[NetworkInvitation] = network.get_membership_requests() + except NoPermissionsError: raise CommandError("Permission denied.") if not requests: @@ -198,12 +198,12 @@ def handle_network_requests(context: DeviceContext, args: List[str]): else: print("Pending requests:") for request in requests: - device: Device = Device.get_device(context.client, request.device) + device: Device = Device.get_device(context.client, request.device_uuid) print(f" - {device.name} (UUID: {device.uuid})") @handle_network.subcommand("accept") -def handle_network_accept(context: DeviceContext, args: List[str]): +def handle_network_accept(context: DeviceContext, args: list[str]) -> None: """ Accept a membership request or an invitation """ @@ -212,7 +212,7 @@ def handle_network_accept(context: DeviceContext, args: List[str]): @handle_network.subcommand("deny") -def handle_network_deny(context: DeviceContext, args: List[str]): +def handle_network_deny(context: DeviceContext, args: list[str]) -> None: """ Deny a membership request or an invitation """ @@ -221,7 +221,7 @@ def handle_network_deny(context: DeviceContext, args: List[str]): @handle_network.subcommand("invite") -def handle_network_invite(context: DeviceContext, args: List[str]): +def handle_network_invite(context: DeviceContext, args: list[str]) -> None: """ Invite a device to one of your networks """ @@ -234,33 +234,33 @@ def handle_network_invite(context: DeviceContext, args: List[str]): device: Device = get_device(context, args[1]) try: network.invite_device(device) - except NetworkNotFoundException: + except NetworkNotFoundError: raise CommandError("Permission denied.") - except AlreadyMemberOfNetworkException: + except AlreadyMemberOfNetworkError: raise CommandError("Device is already a member of this network.") - except InvitationAlreadyExistsException: + except InvitationAlreadyExistsError: raise CommandError("An invitation for this device already exists.") @handle_network.subcommand("invitations") -def handle_network_invitations(context: DeviceContext, _): +def handle_network_invitations(context: DeviceContext, _: Any) -> None: """ View invitations for this device to other networks """ - invitations: List[NetworkInvitation] = context.host.get_network_invitations() + invitations: list[NetworkInvitation] = context.host.get_network_invitations() if not invitations: print("There are no pending network invitations for this device.") else: print("Pending network invitations:") for invitation in invitations: - network: Network = Network.get_by_uuid(context.client, invitation.network) - owner: str = " (owner)" * (network.owner == context.host.uuid) + network: Network = Network.get_by_uuid(context.client, invitation.network_uuid) + owner: str = " (owner)" * (network.owner_uuid == context.host.uuid) print(f" - [{['public', 'private'][network.hidden]}] {network.name}{owner} (UUID: {network.uuid})") @handle_network.subcommand("leave") -def handle_network_leave(context: DeviceContext, args: List[str]): +def handle_network_leave(context: DeviceContext, args: list[str]) -> None: """ Leave a network """ @@ -272,12 +272,12 @@ def handle_network_leave(context: DeviceContext, args: List[str]): try: network.leave(context.host) - except CannotLeaveOwnNetworkException: + except CannotLeaveOwnNetworkError: raise CommandError("You cannot leave your own network.") @handle_network.subcommand("kick") -def handle_network_kick(context: DeviceContext, args: List[str]): +def handle_network_kick(context: DeviceContext, args: list[str]) -> None: """ Kick a device from one of your networks """ @@ -287,23 +287,23 @@ def handle_network_kick(context: DeviceContext, args: List[str]): network: Network = get_network(context, args[0]) - devices: List[Device] = [] + devices: list[Device] = [] for member in network.get_members(): - devices.append(Device.get_device(context.client, member.device)) + devices.append(Device.get_device(context.client, member.device_uuid)) device: Device = get_device(context, args[1], devices) try: network.kick(device) - except NetworkNotFoundException: + except NetworkNotFoundError: raise CommandError("Permission denied.") - except NoPermissionsException: + except NoPermissionsError: raise CommandError("Permission denied.") - except CannotKickOwnerException: + except CannotKickOwnerError: raise CommandError("You cannot kick the owner of the network.") @handle_network.subcommand("delete") -def handle_network_delete(context: DeviceContext, args: List[str]): +def handle_network_delete(context: DeviceContext, args: list[str]) -> None: """ Delete one of your networks """ @@ -315,21 +315,21 @@ def handle_network_delete(context: DeviceContext, args: List[str]): try: network.delete() - except NetworkNotFoundException: + except NetworkNotFoundError: raise CommandError("Permission denied.") -def device_network_names(context: DeviceContext) -> List[str]: +def device_network_names(context: DeviceContext) -> list[str]: return [network.name for network in context.host.get_networks()] -def public_network_names(context: DeviceContext) -> List[str]: +def public_network_names(context: DeviceContext) -> list[str]: return [network.name for network in Network.get_public_networks(context.client)] -def invitation_network_names(context: DeviceContext) -> List[str]: +def invitation_network_names(context: DeviceContext) -> list[str]: return [ - Network.get_by_uuid(context.client, invitation.network).name + Network.get_by_uuid(context.client, invitation.network_uuid).name for invitation in context.host.get_network_invitations() ] @@ -339,14 +339,14 @@ def invitation_network_names(context: DeviceContext) -> List[str]: @handle_network_requests.completer() @handle_network_leave.completer() @handle_network_delete.completer() -def network_completer(context: DeviceContext, args: List[str]) -> List[str]: +def network_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return [*{*device_network_names(context), *public_network_names(context), *invitation_network_names(context)}] return [] @handle_network_create.completer() -def network_create_completer(_, args: List[str]) -> List[str]: +def network_create_completer(_: Any, args: list[str]) -> list[str]: if len(args) == 2: return ["public", "private"] return [] @@ -354,7 +354,7 @@ def network_create_completer(_, args: List[str]) -> List[str]: @handle_network_accept.completer() @handle_network_deny.completer() -def network_accept_deny_completer(context: DeviceContext, args: List[str]) -> List[str]: +def network_accept_deny_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return [*{*device_network_names(context), *invitation_network_names(context)}] if len(args) == 2: @@ -362,28 +362,28 @@ def network_accept_deny_completer(context: DeviceContext, args: List[str]) -> Li network: Network = get_network(context, args[0]) except CommandError: return [] - device_names: List[str] = [] + device_names: list[str] = [] for request in network.get_membership_requests(): try: - device_names.append(Device.get_device(context.client, request.device).name) - except DeviceNotFoundException: + device_names.append(Device.get_device(context.client, request.device_uuid).name) + except DeviceNotFoundError: pass return [name for name in device_names if device_names.count(name) == 1] return [] @handle_network_invite.completer() -def network_invite_completer(context: DeviceContext, args: List[str]): +def network_invite_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return [*{*device_network_names(context)}] if len(args) == 2: - device_names: List[str] = [device.name for device in Device.list_devices(context.client)] + device_names: list[str] = [device.name for device in Device.list_devices(context.client)] return [name for name in device_names if device_names.count(name) == 1] return [] @handle_network_kick.completer() -def network_kick_completer(context: DeviceContext, args: List[str]): +def network_kick_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return [*{*device_network_names(context)}] if len(args) == 2: @@ -391,8 +391,8 @@ def network_kick_completer(context: DeviceContext, args: List[str]): network: Network = get_network(context, args[0]) except CommandError: return [] - device_names: List[str] = [] + device_names: list[str] = [] for member in network.get_members(): - device_names.append(Device.get_device(context.client, member.device).name) + device_names.append(Device.get_device(context.client, member.device_uuid).name) return [name for name in device_names if device_names.count(name) == 1] return [] diff --git a/PyCrypCli/commands/service.py b/PyCrypCli/commands/service.py index 215ce4a..8db39a0 100644 --- a/PyCrypCli/commands/service.py +++ b/PyCrypCli/commands/service.py @@ -1,36 +1,36 @@ import os import time -from typing import List, Tuple - -from PyCrypCli.commands import command, CommandError -from PyCrypCli.commands.help import print_help -from PyCrypCli.commands.morphcoin import get_wallet_from_file -from PyCrypCli.context import DeviceContext, MainContext -from PyCrypCli.exceptions import ( - ServiceNotFoundException, - AttackNotRunningException, - AlreadyOwnThisServiceException, - WalletNotFoundException, - CannotDeleteEnforcedServiceException, - CannotToggleDirectlyException, - CouldNotStartService, - ServiceNotRunningException, +from typing import Any, cast + +from .command import command, CommandError +from .help import print_help +from .morphcoin import get_wallet_from_file +from ..context import DeviceContext, MainContext +from ..exceptions import ( + ServiceNotFoundError, + AttackNotRunningError, + AlreadyOwnThisServiceError, + WalletNotFoundError, + CannotDeleteEnforcedServiceError, + CannotToggleDirectlyError, + CouldNotStartServiceError, + ServiceNotRunningError, ) -from PyCrypCli.game_objects import Device, Service, PortscanService, BruteforceService, PublicService -from PyCrypCli.util import is_uuid +from ..models import Device, Service, PortscanService, BruteforceService, PublicService +from ..util import is_uuid def get_service(context: DeviceContext, name: str) -> Service: try: return context.host.get_service_by_name(name) - except ServiceNotFoundException: + except ServiceNotFoundError: raise CommandError(f"The service '{name}' could not be found on this device") -def stop_bruteforce(context: DeviceContext, service: BruteforceService): +def stop_bruteforce(context: DeviceContext, service: BruteforceService) -> None: try: access, _, target_device = service.stop() - except AttackNotRunningException: + except AttackNotRunningError: raise CommandError("Bruteforce attack is not running.") if access: if context.ask("Access granted. Do you want to connect to the device? [yes|no] ", ["yes", "no"]) == "yes": @@ -42,7 +42,7 @@ def stop_bruteforce(context: DeviceContext, service: BruteforceService): @command("service", [DeviceContext]) -def handle_service(context: DeviceContext, args: List[str]): +def handle_service(context: DeviceContext, args: list[str]) -> None: """ Create or use a service """ @@ -53,7 +53,7 @@ def handle_service(context: DeviceContext, args: List[str]): @handle_service.subcommand("create") -def handle_service_create(context: DeviceContext, args: List[str]): +def handle_service_create(context: DeviceContext, args: list[str]) -> None: """ Create a new service """ @@ -61,7 +61,7 @@ def handle_service_create(context: DeviceContext, args: List[str]): if len(args) not in (1, 2) or args[0] not in ("bruteforce", "portscan", "telnet", "ssh", "miner"): raise CommandError("usage: service create bruteforce|portscan|telnet|ssh|miner") - extra: dict = {} + extra: dict[str, Any] = {} if args[0] == "miner": if len(args) != 2: raise CommandError("usage: service create miner ") @@ -70,7 +70,7 @@ def handle_service_create(context: DeviceContext, args: List[str]): wallet_uuid: str = get_wallet_from_file(context, args[1]).uuid except CommandError: if is_uuid(args[1]): - wallet_uuid: str = args[1] + wallet_uuid = args[1] else: raise CommandError("Invalid wallet uuid") @@ -79,19 +79,19 @@ def handle_service_create(context: DeviceContext, args: List[str]): try: context.host.create_service(args[0], **extra) print("Service has been created") - except AlreadyOwnThisServiceException: + except AlreadyOwnThisServiceError: raise CommandError("You already created this service") - except WalletNotFoundException: + except WalletNotFoundError: raise CommandError("Wallet does not exist.") @handle_service.subcommand("list") -def handle_service_list(context: DeviceContext, _): +def handle_service_list(context: DeviceContext, _: Any) -> None: """ List all services installed on this device """ - services: List[Service] = context.host.get_services() + services: list[Service] = context.host.get_services() if not services: print("There are no services on this device.") else: @@ -104,7 +104,7 @@ def handle_service_list(context: DeviceContext, _): @handle_service.subcommand("delete") -def handle_service_delete(context: DeviceContext, args: List[str]): +def handle_service_delete(context: DeviceContext, args: list[str]) -> None: """ Delete a service """ @@ -116,12 +116,12 @@ def handle_service_delete(context: DeviceContext, args: List[str]): try: service.delete() - except CannotDeleteEnforcedServiceException: + except CannotDeleteEnforcedServiceError: raise CommandError("The service could not be deleted.") @handle_service.subcommand("start") -def handle_service_start(context: DeviceContext, args: List[str]): +def handle_service_start(context: DeviceContext, args: list[str]) -> None: """ Start a service """ @@ -135,12 +135,12 @@ def handle_service_start(context: DeviceContext, args: List[str]): try: service.toggle() - except (CannotToggleDirectlyException, CouldNotStartService): + except (CannotToggleDirectlyError, CouldNotStartServiceError): raise CommandError("The service could not be started.") @handle_service.subcommand("stop") -def handle_service_stop(context: DeviceContext, args: List[str]): +def handle_service_stop(context: DeviceContext, args: list[str]) -> None: """ Stop a service """ @@ -154,12 +154,12 @@ def handle_service_stop(context: DeviceContext, args: List[str]): try: service.toggle() - except CannotToggleDirectlyException: + except CannotToggleDirectlyError: raise CommandError("The service could not be stopped.") @handle_service.subcommand("portscan") -def handle_portscan(context: DeviceContext, args: List[str]): +def handle_portscan(context: DeviceContext, args: list[str]) -> None: """ Perform a portscan """ @@ -173,30 +173,31 @@ def handle_portscan(context: DeviceContext, args: List[str]): try: service: PortscanService = PortscanService.get_portscan_service(context.client, context.host.uuid) - except ServiceNotFoundException: + except ServiceNotFoundError: raise CommandError("You have to create a portscan service before you can use it.") - services: List[PublicService] = service.scan(target) - context.update_last_portscan((target, services)) + services: list[PublicService] = service.scan(target) + context.last_portscan = target, services if not services: print("That device doesn't have any running services") - for service in services: - print(f" - {service.name} on port {service.running_port} (UUID: {service.uuid})") + for s in services: + print(f" - {s.name} on port {s.running_port} (UUID: {s.uuid})") @handle_service.subcommand("bruteforce") -def handle_bruteforce(context: DeviceContext, args: List[str]): +def handle_bruteforce(context: DeviceContext, args: list[str]) -> None: """ Start a bruteforce attack """ - duration: str = "100%" + duration_arg: str = "100%" + duration: int = 0 chance: float | None = None if len(args) in (1, 2) and args[0] in ("ssh", "telnet"): - last_portscan: Tuple[str, List[PublicService]] = context.get_last_portscan() - if last_portscan is None: + if context.last_portscan is None: raise CommandError("You have to portscan your target first to find open ports.") - target_device, services = last_portscan + + target_device, services = context.last_portscan for service in services: if service.name == args[0]: target_service: str = service.uuid @@ -204,64 +205,66 @@ def handle_bruteforce(context: DeviceContext, args: List[str]): else: raise CommandError(f"Service '{args[0]}' is not running on target device.") if len(args) == 2: - duration: str = args[1] + duration_arg = args[1] elif len(args) in (2, 3): - target_device: str = args[0] - target_service: str = args[1] + target_device = args[0] + target_service = args[1] if not is_uuid(target_device): raise CommandError("Invalid target device") if not is_uuid(target_service): raise CommandError("Invalid target service") if len(args) == 3: - duration: str = args[2] + duration_arg = args[2] else: raise CommandError( "usage: service bruteforce [duration|success_chance]\n" - " service bruteforce ssh|telnet [duration|success_chance]", + " service bruteforce ssh|telnet [duration|success_chance]" ) - if duration.endswith("%"): + if duration_arg.endswith("%"): error = "Success chance has to be a positive number between 0 and 100" try: - chance = float(duration[:-1]) / 100 + chance = float(duration_arg[:-1]) / 100 except ValueError: raise CommandError(error) if chance < 0 or chance > 1: raise CommandError(error) else: - if not duration.isnumeric(): + if not duration_arg.isnumeric(): raise CommandError("Duration has to be a positive integer") - duration: int = int(duration) + duration = int(duration_arg) try: - service: BruteforceService = BruteforceService.get_bruteforce_service(context.client, context.host.uuid) - except ServiceNotFoundException: + bruteforce_service: BruteforceService = BruteforceService.get_bruteforce_service( + context.client, context.host.uuid + ) + except ServiceNotFoundError: raise CommandError("You have to create a bruteforce service before you can use it.") - if service.running: + if bruteforce_service.running: print("You are already attacking a device.") - print(f"Target device: {service.target_device}") + print(f"Target device: {bruteforce_service.target_device_uuid}") if context.ask("Do you want to stop this attack? [yes|no] ", ["yes", "no"]) == "yes": - stop_bruteforce(context, service) + stop_bruteforce(context, bruteforce_service) return try: - service.attack(target_device, target_service) + bruteforce_service.attack(target_device, target_service) if chance is not None: - service.update() - duration: int = round((chance + 0.1) * 20 / service.speed) - except ServiceNotFoundException: + bruteforce_service.update() + duration = round((chance + 0.1) * 20 / bruteforce_service.speed) + except ServiceNotFoundError: raise CommandError("The target service does not exist.") - except ServiceNotRunningException: + except ServiceNotRunningError: raise CommandError("The target service is not running and cannot be exploited.") print("You started a bruteforce attack") - width: int = os.get_terminal_size().columns - 31 - steps: int = 17 - d: int = duration * steps - i: int = 0 - last_check = 0 + width = os.get_terminal_size().columns - 31 + steps = 17 + d = duration * steps + i = 0 + last_check: float = 0 try: context.update_presence( state=f"Logged in: {context.username}@{context.root_context.host}", @@ -273,27 +276,27 @@ def handle_bruteforce(context: DeviceContext, args: List[str]): for i in range(d): if time.time() - last_check > 1: last_check = time.time() - service.update() - if not service.running: + bruteforce_service.update() + if not bruteforce_service.running: print("\rBruteforce attack has been aborted.") return progress: int = int(i / d * width) j = i // steps progress_bar = "[" + "=" * progress + ">" + " " * (width - progress) + "]" - text: str = f"\rBruteforcing {j // 60:02d}:{j % 60:02d} {progress_bar} ({i / d * 100:.1f}%) " + text = f"\rBruteforcing {j // 60:02d}:{j % 60:02d} {progress_bar} ({i / d * 100:.1f}%) " print(end=text, flush=True) time.sleep(1 / steps) - i: int = (i + 1) // steps + i = (i + 1) // steps print(f"\rBruteforcing {i // 60:02d}:{i % 60:02d} [" + "=" * width + ">] (100%) ") except KeyboardInterrupt: print() context.main_loop_presence() - stop_bruteforce(context, service) + stop_bruteforce(context, bruteforce_service) @handle_service_create.completer() -def service_create_completer(context: DeviceContext, args: List[str]) -> List[str]: +def service_create_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return ["bruteforce", "portscan", "ssh", "telnet", "miner"] if len(args) == 2 and args[0] == "miner": @@ -302,7 +305,7 @@ def service_create_completer(context: DeviceContext, args: List[str]) -> List[st @handle_service_delete.completer() -def service_delete_completer(_, args: List[str]) -> List[str]: +def service_delete_completer(_: Any, args: list[str]) -> list[str]: if len(args) == 1: return ["bruteforce", "portscan", "telnet", "miner"] return [] @@ -311,14 +314,14 @@ def service_delete_completer(_, args: List[str]) -> List[str]: @handle_service_start.completer() @handle_service_stop.completer() @handle_bruteforce.completer() -def service_completer(_, args: List[str]) -> List[str]: +def service_completer(_: Any, args: list[str]) -> list[str]: if len(args) == 1: return ["ssh", "telnet"] return [] @command("spot", [DeviceContext]) -def handle_spot(context: DeviceContext, _): +def handle_spot(context: DeviceContext, _: Any) -> None: """ Find a random device in the network """ @@ -330,7 +333,7 @@ def handle_spot(context: DeviceContext, _): @command("remote", [MainContext, DeviceContext]) -def handle_remote(context: MainContext, args: List[str]): +def handle_remote(context: MainContext, args: list[str]) -> None: """ Manage and connect to the devices you hacked before """ @@ -341,12 +344,12 @@ def handle_remote(context: MainContext, args: List[str]): @handle_remote.subcommand("list") -def handle_remote_list(context: MainContext, _): +def handle_remote_list(context: MainContext, _: Any) -> None: """ List remote devices """ - devices: List[Device] = context.get_hacked_devices() + devices: list[Device] = context.get_hacked_devices() if not devices: print("You don't have access to any remote device.") @@ -357,7 +360,7 @@ def handle_remote_list(context: MainContext, _): @handle_remote.subcommand("connect") -def handle_remote_connect(context: MainContext, args: List[str]): +def handle_remote_connect(context: MainContext, args: list[str]) -> None: """ Connect to a remote device """ @@ -372,7 +375,7 @@ def handle_remote_connect(context: MainContext, args: List[str]): if device is None: raise CommandError("This device does not exist or you have no permission to access it.") else: - found_devices: List[Device] = [] + found_devices: list[Device] = [] for device in context.get_hacked_devices(): if device.name == name: found_devices.append(device) @@ -382,18 +385,18 @@ def handle_remote_connect(context: MainContext, args: List[str]): if len(found_devices) > 1: raise CommandError(f"There is more than one device with the name '{name}'. You need to specify its UUID.") - device: Device = found_devices[0] + device = found_devices[0] print(f"Connecting to {device.name} (UUID: {device.uuid})") if device.part_owner(): - context.open(DeviceContext(context.root_context, context.session_token, device)) + context.open(DeviceContext(context.root_context, cast(str, context.session_token), device)) else: raise CommandError("This device does not exist or you have no permission to access it.") @handle_remote_connect.completer() -def remote_completer(context: MainContext, args: List[str]) -> List[str]: +def remote_completer(context: MainContext, args: list[str]) -> list[str]: if len(args) == 1: - device_names: List[str] = [device.name for device in context.get_hacked_devices()] + device_names: list[str] = [device.name for device in context.get_hacked_devices()] return [name for name in device_names if device_names.count(name) == 1] return [] diff --git a/PyCrypCli/commands/shell.py b/PyCrypCli/commands/shell.py index bea8577..95ce1f9 100644 --- a/PyCrypCli/commands/shell.py +++ b/PyCrypCli/commands/shell.py @@ -1,11 +1,13 @@ +from typing import Any + import sentry_sdk -from PyCrypCli.commands import command -from PyCrypCli.context import LoginContext, MainContext, DeviceContext, Context +from .command import command +from ..context import LoginContext, MainContext, DeviceContext @command("clear", [LoginContext, MainContext, DeviceContext]) -def handle_main_clear(*_): +def handle_main_clear(*_: Any) -> None: """ Clear the console """ @@ -14,7 +16,7 @@ def handle_main_clear(*_): @command("history", [MainContext, DeviceContext]) -def handle_main_history(context: MainContext, *_): +def handle_main_history(context: MainContext, _: Any) -> None: """ Show the history of commands entered in this session """ @@ -24,14 +26,14 @@ def handle_main_history(context: MainContext, *_): @command("feedback", [LoginContext, MainContext, DeviceContext]) -def handle_feedback(context: Context, *_): +def handle_feedback(context: LoginContext, _: Any) -> None: """ Send feedback to the developer """ print("Please type your feedback about PyCrypCli below. When you are done press Ctrl+C") feedback = ["User Feedback"] - if hasattr(context, "username"): + if isinstance(context, MainContext) and context.username: feedback[0] += " from " + context.username while True: try: diff --git a/PyCrypCli/commands/shop.py b/PyCrypCli/commands/shop.py index 96252c4..8864589 100644 --- a/PyCrypCli/commands/shop.py +++ b/PyCrypCli/commands/shop.py @@ -1,16 +1,16 @@ -from typing import List, Dict +from typing import Any -from PyCrypCli.client import Client -from PyCrypCli.commands import command, CommandError -from PyCrypCli.commands.help import print_help -from PyCrypCli.commands.morphcoin import get_wallet_from_file -from PyCrypCli.context import DeviceContext -from PyCrypCli.exceptions import ItemNotFoundException, NotEnoughCoinsException -from PyCrypCli.game_objects import Wallet, ShopCategory, ShopProduct -from PyCrypCli.util import strip_float, print_tree +from .command import command, CommandError +from .help import print_help +from .morphcoin import get_wallet_from_file +from ..client import Client +from ..context import DeviceContext +from ..exceptions import ItemNotFoundError, NotEnoughCoinsError +from ..models import Wallet, ShopCategory, ShopProduct +from ..util import strip_float, print_tree -def list_shop_products(client: Client) -> Dict[str, ShopProduct]: +def list_shop_products(client: Client) -> dict[str, ShopProduct]: out = {} for category in ShopCategory.shop_list(client): for subcategory in category.subcategories: @@ -22,7 +22,7 @@ def list_shop_products(client: Client) -> Dict[str, ShopProduct]: @command("shop", [DeviceContext]) -def handle_shop(context: DeviceContext, args: List[str]): +def handle_shop(context: DeviceContext, args: list[str]) -> None: """ Buy new hardware and more in the shop """ @@ -33,12 +33,12 @@ def handle_shop(context: DeviceContext, args: List[str]): @handle_shop.subcommand("list") -def handle_shop_list(context: DeviceContext, _): +def handle_shop_list(context: DeviceContext, _: Any) -> None: """ List shop prodcuts """ - categories: List[ShopCategory] = ShopCategory.shop_list(context.client) + categories: list[ShopCategory] = ShopCategory.shop_list(context.client) maxlength = max( *[len(item.name) + 4 for category in categories for item in category.items], *[ @@ -52,7 +52,7 @@ def handle_shop_list(context: DeviceContext, _): for category in categories: category_tree = [] for subcategory in category.subcategories: - subcategory_tree = [ + subcategory_tree: list[tuple[str, list[Any]]] = [ (item.name.ljust(maxlength) + strip_float(item.price / 1000, 3) + " MC", []) for item in subcategory.items ] @@ -68,7 +68,7 @@ def handle_shop_list(context: DeviceContext, _): @handle_shop.subcommand("buy") -def handle_shop_buy(context: DeviceContext, args: List[str]): +def handle_shop_buy(context: DeviceContext, args: list[str]) -> None: """ Buy something in the shop """ @@ -80,21 +80,21 @@ def handle_shop_buy(context: DeviceContext, args: List[str]): wallet: Wallet = get_wallet_from_file(context, wallet_filepath) - shop_products: Dict[str, ShopProduct] = list_shop_products(context.client) + shop_products: dict[str, ShopProduct] = list_shop_products(context.client) if product_name not in shop_products: raise CommandError("This product does not exist in the shop.") product: ShopProduct = shop_products[product_name] try: product.buy(wallet) - except ItemNotFoundException: + except ItemNotFoundError: raise CommandError("This product does not exist in the shop.") - except NotEnoughCoinsException: + except NotEnoughCoinsError: raise CommandError("You don't have enough coins on your wallet to buy this product.") @handle_shop_buy.completer() -def shop_completer(context: DeviceContext, args: List[str]) -> List[str]: +def shop_completer(context: DeviceContext, args: list[str]) -> list[str]: if len(args) == 1: return list(list_shop_products(context.client)) if len(args) == 2: diff --git a/PyCrypCli/commands/status.py b/PyCrypCli/commands/status.py index e5aebe8..3a4b3f1 100644 --- a/PyCrypCli/commands/status.py +++ b/PyCrypCli/commands/status.py @@ -1,9 +1,11 @@ -from PyCrypCli.commands import command -from PyCrypCli.context import MainContext, DeviceContext, LoginContext, Context +from typing import Any + +from .command import command +from ..context import MainContext, DeviceContext, LoginContext, Context @command("whoami", [MainContext, DeviceContext]) -def handle_whoami(context: MainContext, *_): +def handle_whoami(context: MainContext, _: Any) -> None: """ Print the name of the current user """ @@ -12,13 +14,13 @@ def handle_whoami(context: MainContext, *_): @command("status", [LoginContext, MainContext, DeviceContext]) -def handle_status(context: Context, _): +def handle_status(context: Context, _: Any) -> None: """ Indicate how many players are online """ if type(context) is LoginContext: - online: int = context.client.status()["online"] + online: int = context.client.status().online else: - online: int = context.client.info()["online"] + online = context.client.info().online print(f"Online players: {online}") diff --git a/PyCrypCli/context/__init__.py b/PyCrypCli/context/__init__.py index 49289b7..72124e5 100644 --- a/PyCrypCli/context/__init__.py +++ b/PyCrypCli/context/__init__.py @@ -1,15 +1,16 @@ -from PyCrypCli.context.context import Context, COMMAND_FUNCTION, COMPLETER_FUNCTION -from PyCrypCli.context.root_context import RootContext -from PyCrypCli.context.login_context import LoginContext -from PyCrypCli.context.main_context import MainContext -from PyCrypCli.context.device_context import DeviceContext +from .context import Context, COMMAND_FUNCTION, COMPLETER_FUNCTION, ContextType +from .device_context import DeviceContext +from .login_context import LoginContext +from .main_context import MainContext +from .root_context import RootContext __all__ = [ "Context", "COMMAND_FUNCTION", "COMPLETER_FUNCTION", - "RootContext", + "ContextType", + "DeviceContext", "LoginContext", "MainContext", - "DeviceContext", + "RootContext", ] diff --git a/PyCrypCli/context/context.py b/PyCrypCli/context/context.py index 1caca9c..495d639 100644 --- a/PyCrypCli/context/context.py +++ b/PyCrypCli/context/context.py @@ -1,23 +1,25 @@ -from typing import Optional, List, Callable, Dict, TYPE_CHECKING +from __future__ import annotations + +from typing import Callable, TYPE_CHECKING, TypeVar import readline from pypresence import PyPresenceException -from PyCrypCli.client import Client +from ..client import Client if TYPE_CHECKING: - from PyCrypCli.commands import Command - from PyCrypCli.context import RootContext + from .root_context import RootContext + from ..commands import Command class Context: - def __init__(self, root_context: "RootContext"): - self.root_context: "RootContext" = root_context + def __init__(self, root_context: RootContext): + self.root_context: RootContext = root_context - self.override_completions: Optional[List[str]] = None - self.history: List[str] = [] + self.override_completions: list[str] | None = None + self.history: list[str] = [] - def add_to_history(self, command: str): + def add_to_history(self, command: str) -> None: if self.history[-1:] != [command]: self.history.append(command) @@ -29,18 +31,20 @@ def prompt(self) -> str: def client(self) -> Client: return self.root_context.client - def get_commands(self) -> Dict[str, "Command"]: + def get_commands(self) -> dict[str, Command]: return self.root_context.get_commands() - def ask(self, prompt: str, options: List[str]) -> str: + def ask(self, prompt: str, options: list[str]) -> str: while True: try: choice: str = self.input_no_history(prompt, options).strip() except (KeyboardInterrupt, EOFError): print("\nEnter one of the following:", ", ".join(options)) continue + if choice in options: return choice + print(f"'{choice}' is not one of the following:", ", ".join(options)) def confirm(self, question: str) -> bool: @@ -48,63 +52,60 @@ def confirm(self, question: str) -> bool: def update_presence( self, - state: str = None, - details: str = None, - start: int = None, - end: int = None, - large_image: str = None, - large_text: str = None, - ): + state: str | None = None, + details: str | None = None, + start: int | None = None, + end: int | None = None, + large_image: str | None = None, + large_text: str | None = None, + ) -> None: if self.root_context.presence is None: return + try: self.root_context.presence.update( - state=state, - details=details, - start=start, - end=end, - large_image=large_image, - large_text=large_text, + state=state, details=details, start=start, end=end, large_image=large_image, large_text=large_text ) except PyPresenceException: pass - def enter_context(self): + def enter_context(self) -> None: readline.clear_history() - def leave_context(self): + def leave_context(self) -> None: pass - def reenter_context(self): + def reenter_context(self) -> None: self.restore_history() - def input_no_history(self, prompt: str, override_completions: Optional[List[str]] = None) -> Optional[str]: + def input_no_history(self, prompt: str, override_completions: list[str] | None = None) -> str: readline.clear_history() - old_override: List[str] = self.override_completions - self.override_completions: List[str] = override_completions or [] + old_override = self.override_completions + self.override_completions = override_completions or [] try: return input(prompt) finally: self.restore_history() self.override_completions = old_override - def restore_history(self): + def restore_history(self) -> None: readline.clear_history() for command in self.history: readline.add_history(command) - def open(self, context: "Context"): + def open(self, context: Context) -> None: self.root_context.open(context) - def close(self): + def close(self) -> None: self.root_context.close() - def loop_tick(self): + def loop_tick(self) -> None: pass def before_command(self) -> bool: return True -COMMAND_FUNCTION = Callable[[Context, List[str]], None] -COMPLETER_FUNCTION = Callable[[Context, List[str]], List[str]] +ContextType = TypeVar("ContextType", bound=Context) +COMMAND_FUNCTION = Callable[[ContextType, list[str]], None] +COMPLETER_FUNCTION = Callable[[ContextType, list[str]], list[str]] diff --git a/PyCrypCli/context/device_context.py b/PyCrypCli/context/device_context.py index 6b74954..9fc3839 100644 --- a/PyCrypCli/context/device_context.py +++ b/PyCrypCli/context/device_context.py @@ -1,13 +1,11 @@ -from typing import Optional, Tuple, List - import readline -from PyCrypCli.context.context import Context -from PyCrypCli.context.main_context import MainContext -from PyCrypCli.context.root_context import RootContext -from PyCrypCli.exceptions import FileNotFoundException, InvalidWalletFile -from PyCrypCli.game_objects import Device, File, Service, PublicService -from PyCrypCli.util import extract_wallet +from .context import Context +from .main_context import MainContext +from .root_context import RootContext +from ..exceptions import InvalidWalletFileError +from ..models import Device, File, Service, PublicService +from ..util import extract_wallet class DeviceContext(MainContext): @@ -16,37 +14,38 @@ def __init__(self, root_context: RootContext, session_token: str, device: Device self.host: Device = device self.pwd: File = self.get_root_dir() - self.last_portscan: Optional[Tuple[str, List[Service]]] = None + self.last_portscan: tuple[str, list[PublicService]] | None = None - def update_pwd(self): - if self.pwd.uuid is not None: + def update_pwd(self) -> None: + if self.pwd.uuid: self.pwd = self.host.get_file(self.pwd.uuid) - def is_localhost(self): - return self.user_uuid == self.host.owner + def is_localhost(self) -> bool: + return self.user_uuid == self.host.owner_uuid @property def prompt(self) -> str: self.update_pwd() if self.is_localhost(): - color: str = "\033[38;2;100;221;23m" + color = "\033[38;2;100;221;23m" else: - color: str = "\033[38;2;255;64;23m" + color = "\033[38;2;255;64;23m" return f"{color}[{self.username}@{self.host.name}:{self.file_to_path(self.pwd)}]$\033[0m " - def loop_tick(self): + def loop_tick(self) -> None: super().loop_tick() self.host.update() self.check_device_permission() def check_device_permission(self) -> bool: - if self.host.owner != self.user_uuid and all( - service.device != self.host.uuid for service in Service.list_part_owner(self.client) + if self.host.owner_uuid != self.user_uuid and all( + service.device_uuid != self.host.uuid for service in Service.list_part_owner(self.client) ): print("You don't have access to this device anymore.") self.close() return False + return True def check_powered_on(self) -> bool: @@ -54,22 +53,23 @@ def check_powered_on(self) -> bool: print("This device is not powered on.") self.close() return False + return True def before_command(self) -> bool: return self.check_device_permission() and self.check_powered_on() - def enter_context(self): + def enter_context(self) -> None: Context.enter_context(self) - def leave_context(self): + def leave_context(self) -> None: pass - def reenter_context(self): + def reenter_context(self) -> None: Context.reenter_context(self) - def get_files(self, directory: str) -> List[File]: - return self.host.get_files(directory) + def get_files(self, parent_dir_uuid: str | None) -> list[File]: + return self.host.get_files(parent_dir_uuid) def get_parent_dir(self, file: File) -> File: if file.parent_dir_uuid is None: @@ -77,51 +77,45 @@ def get_parent_dir(self, file: File) -> File: return self.host.get_file(file.parent_dir_uuid) def get_root_dir(self) -> File: - return File(self.client, {"device": self.host.uuid, "is_directory": True}) + return File.get_root_directory(self.client, self.host.uuid) - def get_file(self, filename: str, directory: str) -> Optional[File]: - files: List[File] = self.get_files(directory) + def get_file(self, filename: str, directory_uuid: str | None) -> File | None: + files: list[File] = self.get_files(directory_uuid) for file in files: - if file.filename == filename: + if file.name == filename: return file return None - def get_filenames(self, directory: str) -> List[str]: - return [file.filename for file in self.get_files(directory)] + def get_filenames(self, directory: str) -> list[str]: + return [file.name for file in self.get_files(directory)] - def get_wallet_credentials_from_file(self, filepath: str) -> Tuple[str, str]: - file: File = self.path_to_file(filepath) + def get_wallet_credentials_from_file(self, filepath: str) -> tuple[str, str]: + file: File | None = self.path_to_file(filepath) if file is None: - raise FileNotFoundException + raise FileNotFoundError - wallet: Optional[Tuple[str, str]] = extract_wallet(file.content) + wallet: tuple[str, str] | None = extract_wallet(file.content) if wallet is None: - raise InvalidWalletFile + raise InvalidWalletFileError return wallet - def get_last_portscan(self) -> Tuple[str, List[PublicService]]: - return self.last_portscan - - def update_last_portscan(self, scan: Tuple[str, List[PublicService]]): - self.last_portscan: Tuple[str, List[PublicService]] = scan - - def file_path_completer(self, path: str, dirs_only: bool = False) -> List[str]: + def file_path_completer(self, path: str, dirs_only: bool = False) -> list[str]: base_path: str = "/".join(path.split("/")[:-1]) if path.startswith("/"): - base_path: str = "/" + base_path - base_dir: Optional[File] = self.path_to_file(base_path) + base_path = "/" + base_path + base_dir: File | None = self.path_to_file(base_path) if base_dir is None or not base_dir.is_directory: return [] if base_path: readline.set_completer_delims("/") return [ - file.filename + "/\0" * file.is_directory + file.name + "/\0" * file.is_directory for file in self.get_files(base_dir.uuid) if file.is_directory or not dirs_only ] + ["./\0", "../\0"] * path.split("/")[-1].startswith(".") - def path_to_file(self, path: str) -> Optional[File]: + def path_to_file(self, path: str) -> File | None: pwd: File = self.get_root_dir() if path.startswith("/") else self.pwd for file_name in path.split("/"): if not file_name or file_name == ".": @@ -129,15 +123,16 @@ def path_to_file(self, path: str) -> Optional[File]: if file_name == "..": pwd = self.get_parent_dir(pwd) else: - pwd: Optional[File] = self.get_file(file_name, pwd.uuid) - if pwd is None: + if not (file := self.get_file(file_name, pwd.uuid)): return None + pwd = file return pwd - def file_to_path(self, file: Optional[File]) -> str: - if file.uuid is None: + def file_to_path(self, file: File) -> str: + if file.is_root_directory: return "/" - path: List[File] = [file] + + path: list[File] = [file] while path[-1].parent_dir_uuid is not None: path.append(self.host.get_file(path[-1].parent_dir_uuid)) - return "/" + "/".join(f.filename for f in path[::-1]) + return "/" + "/".join(f.name for f in path[::-1]) diff --git a/PyCrypCli/context/login_context.py b/PyCrypCli/context/login_context.py index 5fb3e65..adfee87 100644 --- a/PyCrypCli/context/login_context.py +++ b/PyCrypCli/context/login_context.py @@ -1,8 +1,9 @@ import time -from PyCrypCli.context.context import Context -from PyCrypCli.context.root_context import RootContext -from PyCrypCli.exceptions import InvalidSessionTokenException +from .context import Context +from .root_context import RootContext +from ..exceptions import InvalidSessionTokenError +from ..models import Config class LoginContext(Context): @@ -13,37 +14,36 @@ def __init__(self, root_context: RootContext): def prompt(self) -> str: return "$ " - def enter_context(self): + def enter_context(self) -> None: self.login_loop_presence() try: self.load_session() - except InvalidSessionTokenException: + except InvalidSessionTokenError: self.delete_session() print("You are not logged in.") print("Type `register` to create a new account or `login` if you already have one.") - def reenter_context(self): + def reenter_context(self) -> None: super().reenter_context() self.login_loop_presence() - def load_session(self): - from PyCrypCli.context.main_context import MainContext + def load_session(self) -> None: + from .main_context import MainContext - config: dict = self.root_context.read_config_file() + config: Config = self.root_context.read_config_file() - if "token" in config.setdefault("servers", {}).setdefault(self.root_context.host, {}): - session_token: str = config["servers"][self.root_context.host]["token"] - self.client.session(session_token) - self.open(MainContext(self.root_context, session_token)) + if (server_config := config.servers.get(self.root_context.host)) and server_config.token: + self.client.session(server_config.token) + self.open(MainContext(self.root_context, server_config.token)) - def delete_session(self): - config: dict = self.root_context.read_config_file() - if "token" in config.setdefault("servers", {}).setdefault(self.root_context.host, {}): - del config["servers"][self.root_context.host]["token"] + def delete_session(self) -> None: + config: Config = self.root_context.read_config_file() + if server_config := config.servers.get(self.root_context.host): + server_config.token = None self.root_context.write_config_file(config) - def login_loop_presence(self): + def login_loop_presence(self) -> None: self.update_presence( state=f"Server: {self.root_context.host}", details="Logging in", diff --git a/PyCrypCli/context/main_context.py b/PyCrypCli/context/main_context.py index 1f66a3c..e9e1502 100644 --- a/PyCrypCli/context/main_context.py +++ b/PyCrypCli/context/main_context.py @@ -1,35 +1,34 @@ import time -from typing import Optional, Tuple, List -from PyCrypCli.context.context import Context -from PyCrypCli.context.login_context import LoginContext -from PyCrypCli.context.root_context import RootContext -from PyCrypCli.exceptions import InvalidWalletFile -from PyCrypCli.game_objects import Wallet, Device, Service -from PyCrypCli.util import extract_wallet +from .context import Context +from .login_context import LoginContext +from .root_context import RootContext +from ..exceptions import InvalidWalletFileError, LoggedOutError +from ..models import Wallet, Device, Service, Config, ServerConfig, InfoResponse +from ..util import extract_wallet class MainContext(LoginContext): def __init__(self, root_context: RootContext, session_token: str): super().__init__(root_context) - self.username: Optional[str] = None - self.user_uuid: Optional[str] = None - self.session_token: Optional[str] = session_token + self.username: str | None = None + self.user_uuid: str | None = None + self.session_token: str | None = session_token @property def prompt(self) -> str: return f"\033[38;2;53;160;171m[{self.username}]$\033[0m " - def update_user_info(self): - info: dict = self.root_context.client.info() - self.username: str = info["name"] - self.user_uuid: str = info["uuid"] + def update_user_info(self) -> None: + info: InfoResponse = self.root_context.client.info() + self.username = info.name + self.user_uuid = info.uuid - def loop_tick(self): + def loop_tick(self) -> None: self.update_user_info() - def enter_context(self): + def enter_context(self) -> None: Context.enter_context(self) self.update_user_info() @@ -39,27 +38,30 @@ def enter_context(self): print(f"Logged in as {self.username}.") - def leave_context(self): + def leave_context(self) -> None: if self.client.logged_in: self.client.logout() self.delete_session() print("Logged out.") - def reenter_context(self): + def reenter_context(self) -> None: Context.reenter_context(self) self.main_loop_presence() - def save_session(self): - config: dict = self.root_context.read_config_file() - config.setdefault("servers", {}).setdefault(self.root_context.host, {})["token"] = self.session_token + def save_session(self) -> None: + if not self.session_token: + raise LoggedOutError + + config: Config = self.root_context.read_config_file() + config.servers[self.root_context.host] = ServerConfig(token=self.session_token) self.root_context.write_config_file(config) - def delete_session(self): + def delete_session(self) -> None: super().delete_session() self.session_token = None - def main_loop_presence(self): + def main_loop_presence(self) -> None: self.update_presence( state=f"Logged in: {self.username}@{self.root_context.host}", details="in Cryptic Terminal", @@ -69,12 +71,12 @@ def main_loop_presence(self): ) def extract_wallet(self, content: str) -> Wallet: - wallet: Optional[Tuple[str, str]] = extract_wallet(content) + wallet: tuple[str, str] | None = extract_wallet(content) if wallet is None: - raise InvalidWalletFile + raise InvalidWalletFileError return Wallet.get_wallet(self.client, *wallet) - def get_hacked_devices(self) -> List[Device]: + def get_hacked_devices(self) -> list[Device]: return list( - {Device.get_device(self.client, service.device) for service in Service.list_part_owner(self.client)}, + {Device.get_device(self.client, service.device_uuid) for service in Service.list_part_owner(self.client)} ) diff --git a/PyCrypCli/context/root_context.py b/PyCrypCli/context/root_context.py index c808b41..1690190 100644 --- a/PyCrypCli/context/root_context.py +++ b/PyCrypCli/context/root_context.py @@ -1,32 +1,34 @@ -import json -import os +from __future__ import annotations + import re -from typing import List, Dict, Type, Optional, TYPE_CHECKING +from pathlib import Path +from typing import Type, TYPE_CHECKING from pypresence import Presence, PyPresenceException -from PyCrypCli.client import Client -from PyCrypCli.context.context import Context +from .context import Context +from ..client import Client +from ..exceptions import InvalidServerURLError +from ..models import Config if TYPE_CHECKING: - from PyCrypCli.commands import Command + from ..commands import Command class RootContext: - def __init__( - self, - server: str, - config_file: List[str], - commands: Dict[Type[Context], Dict[str, "Command"]], - ): + def __init__(self, server: str, config_file: Path, commands: dict[Type[Context], dict[str, Command]]): self.client: Client = Client(server) - self.host: str = re.match(r"^wss?://(.+)$", server).group(1).split("/")[0] - self.config_file: List[str] = config_file + if not (match := re.match(r"^wss?://(.+)$", server)): + raise InvalidServerURLError + + self.host: str = match.group(1).split("/")[0] + + self.config_file: Path = config_file - self.context_stack: List[Context] = [] + self.context_stack: list[Context] = [] - self.commands: Dict[Type[Context], Dict[str, "Command"]] = commands + self.commands: dict[Type[Context], dict[str, Command]] = commands try: self.presence: Presence = Presence(client_id="596676243144048640") @@ -34,35 +36,29 @@ def __init__( except (PyPresenceException, FileNotFoundError, ConnectionRefusedError): self.presence = None - def open(self, context: "Context"): + def open(self, context: Context) -> None: self.context_stack.append(context) context.enter_context() - def close(self): + def close(self) -> None: self.context_stack.pop().leave_context() self.get_context().reenter_context() def get_context(self) -> "Context": return self.context_stack[-1] - def get_override_completions(self) -> Optional[List[str]]: + def get_override_completions(self) -> list[str] | None: return self.get_context().override_completions - def get_commands(self) -> Dict[str, "Command"]: + def get_commands(self) -> dict[str, Command]: return self.commands[type(self.get_context())] - def read_config_file(self) -> dict: - for i in range(1, len(self.config_file)): - path: str = os.path.join(*self.config_file[:i]) - if not os.path.exists(path): - os.mkdir(path) - path: str = os.path.join(*self.config_file) - if not os.path.exists(path): - self.write_config_file({"servers": {}}) - return json.load(open(path)) - - def write_config_file(self, config: dict): - path: str = os.path.join(*self.config_file) - with open(path, "w") as file: - json.dump(config, file) - file.flush() + def read_config_file(self) -> Config: + if not self.config_file.exists(): + self.write_config_file(Config.get_default_config()) + + return Config.parse_raw(self.config_file.read_text()) + + def write_config_file(self, config: Config) -> None: + self.config_file.parent.mkdir(parents=True, exist_ok=True) + self.config_file.write_text(config.json()) diff --git a/PyCrypCli/exceptions.py b/PyCrypCli/exceptions.py index 2ae0c24..80e463c 100644 --- a/PyCrypCli/exceptions.py +++ b/PyCrypCli/exceptions.py @@ -1,275 +1,278 @@ import json -from typing import Optional +from typing import Any -class CommandRegistrationException(Exception): +class ClientNotReadyError(Exception): + pass + + +class InvalidServerURLError(Exception): + pass + + +class CommandRegistrationError(Exception): def __init__(self, name: str, subcommand: bool = False): super().__init__(f"The {'sub' * subcommand}command {name} has already been registered.") -class NoDocStringException(Exception): +class NoDocStringError(Exception): def __init__(self, name: str, subcommand: bool = False): super().__init__(f"The {'sub' * subcommand}command {name} is missing a docstring.") -class LoggedInException(Exception): - def __init__(self): +class LoggedInError(Exception): + def __init__(self) -> None: super().__init__("Endpoint cannot be used while client is logged in.") -class LoggedOutException(Exception): - def __init__(self): +class LoggedOutError(Exception): + def __init__(self) -> None: super().__init__("Endpoint can only be used while client is logged in.") -class InvalidServerResponseException(Exception): - def __init__(self, response: dict): +class InvalidServerResponseError(Exception): + def __init__(self, response: dict[Any, Any]): super().__init__("Invalid Server Response: " + json.dumps(response)) -class InvalidSessionTokenException(Exception): - def __init__(self): +class InvalidSessionTokenError(Exception): + def __init__(self) -> None: super().__init__("Invalid session token") -class WeakPasswordException(Exception): - def __init__(self): +class WeakPasswordError(Exception): + def __init__(self) -> None: super().__init__("Password is too weak") -class UsernameAlreadyExistsException(Exception): - def __init__(self): +class UsernameAlreadyExistsError(Exception): + def __init__(self) -> None: super().__init__("Username already exists") -class InvalidLoginException(Exception): - def __init__(self): +class InvalidLoginError(Exception): + def __init__(self) -> None: super().__init__("Invalid Login Credentials") -class PermissionsDeniedException(Exception): - def __init__(self): - super().__init__("Permissions Denied") - - -class InvalidWalletFile(Exception): - def __init__(self): +class InvalidWalletFileError(Exception): + def __init__(self) -> None: super().__init__("Invalid wallet file") -class UnknownMicroserviceException(Exception): +class UnknownMicroserviceError(Exception): def __init__(self, ms: str): super().__init__("Unknown Microservice: " + ms) class MicroserviceException(Exception): - error: str = None + error: str | None = None - def __init__(self, error: Optional[str] = None, args: Optional[list] = None): + def __init__(self, error: str | None = None, args: list[Any] | None = None): super().__init__(error or "") self.error = error self.params = args -class InternalErrorException(MicroserviceException): +class InternalError(MicroserviceException): error: str = "internal error" -class NoResponseTimeoutException(MicroserviceException): +class NoResponseTimeoutError(MicroserviceException): error: str = "no response - timeout" -class InvalidRequestException(MicroserviceException): +class InvalidRequestError(MicroserviceException): error: str = "invalid_request" -class AlreadyOwnADeviceException(MicroserviceException): +class AlreadyOwnADeviceError(MicroserviceException): error: str = "already_own_a_device" -class PermissionDeniedException(MicroserviceException): +class PermissionDeniedError(MicroserviceException): error: str = "permission_denied" -class DeviceNotFoundException(MicroserviceException): +class DeviceNotFoundError(MicroserviceException): error: str = "device_not_found" -class DevicePoweredOffException(MicroserviceException): +class DevicePoweredOffError(MicroserviceException): error: str = "device_powered_off" -class DeviceNotOnlineException(MicroserviceException): +class DeviceNotOnlineError(MicroserviceException): error: str = "device_not_online" -class DeviceIsStarterDeviceException(MicroserviceException): +class DeviceIsStarterDeviceError(MicroserviceException): error: str = "device_is_starter_device" -class MaximumDevicesReachedException(MicroserviceException): +class MaximumDevicesReachedError(MicroserviceException): error: str = "maximum_devices_reached" -class ElementPartNotFoundException(MicroserviceException): +class ElementPartNotFoundError(MicroserviceException): error: str = "element_(.+)_not_found" -class PartNotInInventoryException(MicroserviceException): +class PartNotInInventoryError(MicroserviceException): error: str = "(.+)_not_in_inventory" -class MissingPartException(MicroserviceException): +class MissingPartError(MicroserviceException): error: str = "missing_(.+)" -class IncompatibleCPUSocket(MicroserviceException): +class IncompatibleCPUSocketError(MicroserviceException): error: str = "incompatible_cpu_socket" -class NotEnoughRAMSlots(MicroserviceException): +class NotEnoughRAMSlotsError(MicroserviceException): error: str = "not_enough_ram_slots" -class IncompatibleRAMTypes(MicroserviceException): +class IncompatibleRAMTypesError(MicroserviceException): error: str = "incompatible_ram_types" -class IncompatibleDriverInterface(MicroserviceException): +class IncompatibleDriverInterfaceError(MicroserviceException): error: str = "incompatible_drive_interface" -class FileNotFoundException(MicroserviceException): +class FileNotFoundError(MicroserviceException): error: str = "file_not_found" -class FileNotChangeableException(MicroserviceException): +class FileNotChangeableError(MicroserviceException): error: str = "file_not_changeable" -class FileAlreadyExistsException(MicroserviceException): +class FileAlreadyExistsError(MicroserviceException): error: str = "file_already_exists" -class ParentDirectoryNotFound(MicroserviceException): +class ParentDirectoryNotFoundError(MicroserviceException): error: str = "parent_directory_not_found" -class CanNotMoveDirIntoItselfException(MicroserviceException): +class CanNotMoveDirIntoItselfError(MicroserviceException): error: str = "can_not_move_dir_into_itself" -class DirectoriesCanNotBeUpdatedException(MicroserviceException): +class DirectoriesCanNotBeUpdatedError(MicroserviceException): error: str = "directories_can_not_be_updated" -class DirectoryCanNotHaveTextContentException(MicroserviceException): +class DirectoryCanNotHaveTextContentError(MicroserviceException): error: str = "directory_can_not_have_textcontent" -class AlreadyOwnAWalletException(MicroserviceException): +class AlreadyOwnAWalletError(MicroserviceException): error: str = "already_own_a_wallet" -class UnknownSourceOrDestinationException(MicroserviceException): +class UnknownSourceOrDestinationError(MicroserviceException): error: str = "unknown_source_or_destination" -class NotEnoughCoinsException(MicroserviceException): +class NotEnoughCoinsError(MicroserviceException): error: str = "not_enough_coins" -class AlreadyOwnThisServiceException(MicroserviceException): +class AlreadyOwnThisServiceError(MicroserviceException): error: str = "already_own_this_service" -class ServiceNotSupportedException(MicroserviceException): +class ServiceNotSupportedError(MicroserviceException): error: str = "service_not_supported" -class ServiceNotRunningException(MicroserviceException): +class ServiceNotRunningError(MicroserviceException): error: str = "service_not_running" -class CannotToggleDirectlyException(MicroserviceException): +class CannotToggleDirectlyError(MicroserviceException): error: str = "cannot_toggle_directly" -class CouldNotStartService(MicroserviceException): +class CouldNotStartServiceError(MicroserviceException): error: str = "could_not_start_service" -class WalletNotFoundException(MicroserviceException): +class WalletNotFoundError(MicroserviceException): error: str = "wallet_not_found" -class MinerNotFoundException(MicroserviceException): +class MinerNotFoundError(MicroserviceException): error: str = "miner_not_found" -class ServiceNotFoundException(MicroserviceException): +class ServiceNotFoundError(MicroserviceException): error: str = "service_not_found" -class UnknownServiceException(MicroserviceException): +class UnknownServiceError(MicroserviceException): error: str = "unknown_service" -class ServiceCannotBeUsedException(MicroserviceException): +class ServiceCannotBeUsedError(MicroserviceException): error: str = "service_cannot_be_used" -class CannotDeleteEnforcedServiceException(MicroserviceException): +class CannotDeleteEnforcedServiceError(MicroserviceException): error: str = "cannot_delete_enforced_service" -class AttackNotRunningException(MicroserviceException): +class AttackNotRunningError(MicroserviceException): error: str = "attack_not_running" -class ItemNotFoundException(MicroserviceException): +class ItemNotFoundError(MicroserviceException): error: str = "item_not_found" -class CannotTradeWithYourselfException(MicroserviceException): +class CannotTradeWithYourselfError(MicroserviceException): error: str = "cannot_trade_with_yourself" -class UserUUIDDoesNotExistException(MicroserviceException): +class UserUUIDDoesNotExistError(MicroserviceException): error: str = "user_uuid_does_not_exist" -class NetworkNotFoundException(MicroserviceException): +class NetworkNotFoundError(MicroserviceException): error: str = "network_not_found" -class AlreadyMemberOfNetworkException(MicroserviceException): +class AlreadyMemberOfNetworkError(MicroserviceException): error: str = "already_member_of_network" -class InvitationAlreadyExistsException(MicroserviceException): +class InvitationAlreadyExistsError(MicroserviceException): error: str = "invitation_already_exists" -class CannotLeaveOwnNetworkException(MicroserviceException): +class CannotLeaveOwnNetworkError(MicroserviceException): error: str = "cannot_leave_own_network" -class CannotKickOwnerException(MicroserviceException): +class CannotKickOwnerError(MicroserviceException): error: str = "cannot_kick_owner" -class MaximumNetworksReachedException(MicroserviceException): +class MaximumNetworksReachedError(MicroserviceException): error: str = "maximum_networks_reached" -class InvalidNameException(MicroserviceException): +class InvalidNameError(MicroserviceException): error: str = "invalid_name" -class NameAlreadyInUseException(MicroserviceException): +class NameAlreadyInUseError(MicroserviceException): error: str = "name_already_in_use" -class NoPermissionsException(MicroserviceException): +class NoPermissionsError(MicroserviceException): error: str = "no_permissions" diff --git a/PyCrypCli/game_objects/__init__.py b/PyCrypCli/game_objects/__init__.py deleted file mode 100644 index 0e9bbe7..0000000 --- a/PyCrypCli/game_objects/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from PyCrypCli.game_objects.device import Device -from PyCrypCli.game_objects.device_hardware import DeviceHardware -from PyCrypCli.game_objects.file import File -from PyCrypCli.game_objects.game_object import GameObject -from PyCrypCli.game_objects.inventory_element import InventoryElement -from PyCrypCli.game_objects.network import Network, NetworkInvitation, NetworkMembership -from PyCrypCli.game_objects.resource_usage import ResourceUsage -from PyCrypCli.game_objects.service import Service, PublicService, Miner, BruteforceService, PortscanService -from PyCrypCli.game_objects.shop import ShopProduct, ShopCategory -from PyCrypCli.game_objects.transaction import Transaction -from PyCrypCli.game_objects.wallet import Wallet, PublicWallet - - -__all__ = [ - "Device", - "DeviceHardware", - "File", - "GameObject", - "InventoryElement", - "Network", - "NetworkMembership", - "NetworkInvitation", - "ResourceUsage", - "Service", - "PublicService", - "Miner", - "BruteforceService", - "PortscanService", - "ShopProduct", - "ShopCategory", - "Transaction", - "Wallet", - "PublicWallet", -] diff --git a/PyCrypCli/game_objects/device.py b/PyCrypCli/game_objects/device.py deleted file mode 100644 index c82f949..0000000 --- a/PyCrypCli/game_objects/device.py +++ /dev/null @@ -1,129 +0,0 @@ -from typing import List, Optional - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.device_hardware import DeviceHardware -from PyCrypCli.game_objects.file import File -from PyCrypCli.game_objects.game_object import GameObject -from PyCrypCli.game_objects.network import Network, NetworkInvitation -from PyCrypCli.game_objects.resource_usage import ResourceUsage -from PyCrypCli.game_objects.service import PublicService, Service, Miner - - -class Device(GameObject): - @property - def uuid(self) -> str: - return self._data.get("uuid") - - @property - def name(self) -> str: - return self._data.get("name") - - @property - def owner(self) -> str: - return self._data.get("owner") - - @property - def powered_on(self) -> bool: - return self._data.get("powered_on") - - @staticmethod - def get_device(client: Client, device_uuid: str) -> "Device": - return Device(client, client.ms("device", ["device", "info"], device_uuid=device_uuid)) - - @staticmethod - def list_devices(client: Client) -> List["Device"]: - return [Device(client, device) for device in client.ms("device", ["device", "all"])["devices"]] - - def update(self): - self._update(Device.get_device(self._client, self.uuid)) - - @staticmethod - def build(client: Client, mainboard: str, cpu: str, gpu: str, ram: List[str], disk: List[str]) -> "Device": - return Device( - client, - client.ms("device", ["device", "create"], motherboard=mainboard, cpu=cpu, gpu=gpu, ram=ram, disk=disk), - ) - - @staticmethod - def starter_device(client: Client) -> "Device": - return Device(client, client.ms("device", ["device", "starter_device"])) - - @staticmethod - def spot(client: Client) -> "Device": - return Device(client, client.ms("device", ["device", "spot"])) - - def power(self): - self._update(self._ms("device", ["device", "power"], device_uuid=self.uuid)) - - def change_name(self, name: str): - self._update(self._ms("device", ["device", "change_name"], device_uuid=self.uuid, name=name)) - - def delete(self): - self._ms("device", ["device", "delete"], device_uuid=self.uuid) - - def get_files(self, parent_dir_uuid: Optional[str]) -> List[File]: - return [ - File(self._client, file) - for file in self._ms("device", ["file", "all"], device_uuid=self.uuid, parent_dir_uuid=parent_dir_uuid)[ - "files" - ] - ] - - def get_file(self, file_uuid: str) -> File: - return File.get_file(self._client, self.uuid, file_uuid) - - def create_file(self, filename: str, content: str, is_directory: bool, parent_dir_uuid: Optional[str]) -> File: - return File( - self._client, - self._ms( - "device", - ["file", "create"], - device_uuid=self.uuid, - filename=filename, - content=content, - is_directory=is_directory, - parent_dir_uuid=parent_dir_uuid, - ), - ) - - def get_public_service(self, service_uuid: str) -> PublicService: - return PublicService.get_public_service(self._client, self.uuid, service_uuid) - - def get_services(self) -> List[Service]: - return Service.get_services(self._client, self.uuid) - - def get_service(self, service_uuid: str) -> Service: - return Service.get_service(self._client, self.uuid, service_uuid) - - def get_service_by_name(self, service: str) -> Service: - return Service.get_service_by_name(self._client, self.uuid, service) - - def get_miner(self) -> Miner: - return Miner.get_miner(self._client, self.uuid) - - def create_service(self, name: str, **extra) -> Service: - return Service(self._client, self._ms("service", ["create"], name=name, device_uuid=self.uuid, **extra)) - - def part_owner(self) -> bool: - return self._ms("service", ["part_owner"], device_uuid=self.uuid)["ok"] - - def get_hardware(self) -> List[DeviceHardware]: - return [ - DeviceHardware(self._client, dh) - for dh in self._ms("device", ["device", "info"], device_uuid=self.uuid)["hardware"] - ] - - def get_resource_usage(self) -> ResourceUsage: - return ResourceUsage(self._client, self._ms("device", ["hardware", "resources"], device_uuid=self.uuid)) - - def get_networks(self) -> List[Network]: - return [Network(self._client, net) for net in self._ms("network", ["member"], device=self.uuid)["networks"]] - - def create_network(self, name: str, hidden: bool) -> Network: - return Network(self._client, self._ms("network", ["create"], device=self.uuid, name=name, hidden=hidden)) - - def get_network_invitations(self) -> List[NetworkInvitation]: - return [ - NetworkInvitation(self._client, invitation) - for invitation in self._ms("network", ["invitations"], device=self.uuid)["invitations"] - ] diff --git a/PyCrypCli/game_objects/device_hardware.py b/PyCrypCli/game_objects/device_hardware.py deleted file mode 100644 index 59e8cec..0000000 --- a/PyCrypCli/game_objects/device_hardware.py +++ /dev/null @@ -1,19 +0,0 @@ -from PyCrypCli.game_objects.game_object import GameObject - - -class DeviceHardware(GameObject): - @property - def uuid(self) -> str: - return self._data.get("uuid") - - @property - def device(self) -> str: - return self._data.get("device_uuid") - - @property - def hardware_element(self) -> str: - return self._data.get("hardware_element") - - @property - def hardware_type(self) -> str: - return self._data.get("hardware_type") diff --git a/PyCrypCli/game_objects/file.py b/PyCrypCli/game_objects/file.py deleted file mode 100644 index a89d405..0000000 --- a/PyCrypCli/game_objects/file.py +++ /dev/null @@ -1,61 +0,0 @@ -from PyCrypCli.client import Client -from PyCrypCli.game_objects.game_object import GameObject - - -class File(GameObject): - @property - def uuid(self) -> str: - return self._data.get("uuid") - - @property - def device(self) -> str: - return self._data.get("device") - - @property - def filename(self) -> str: - return self._data.get("filename") - - @property - def content(self) -> str: - return self._data.get("content") - - @property - def is_directory(self) -> bool: - return self._data.get("is_directory") - - @property - def parent_dir_uuid(self) -> str: - return self._data.get("parent_dir_uuid") - - @staticmethod - def get_file(client: Client, device_uuid: str, file_uuid: str) -> "File": - return File(client, client.ms("device", ["file", "info"], device_uuid=device_uuid, file_uuid=file_uuid)) - - def update(self): - self._update(File.get_file(self._client, self.device, self.uuid)) - - def move(self, new_filename: str, new_parent_dir_uuid: str): - self._update( - self._ms( - "device", - ["file", "move"], - device_uuid=self.device, - file_uuid=self.uuid, - new_filename=new_filename, - new_parent_dir_uuid=new_parent_dir_uuid, - ), - ) - - def edit(self, new_content: str): - self._update( - self._ms( - "device", - ["file", "update"], - device_uuid=self.device, - file_uuid=self.uuid, - content=new_content, - ), - ) - - def delete(self): - self._ms("device", ["file", "delete"], device_uuid=self.device, file_uuid=self.uuid) diff --git a/PyCrypCli/game_objects/game_object.py b/PyCrypCli/game_objects/game_object.py deleted file mode 100644 index d53c039..0000000 --- a/PyCrypCli/game_objects/game_object.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import List, Union, Type, TypeVar - -from PyCrypCli.client import Client - -T = TypeVar("T") - - -class GameObject: - def __init__(self, client: Client, data: dict): - self._client: Client = client - self._data: dict = data - - def __repr__(self): - out: str = self.__class__.__name__ + "(" - members = [] - for key in dir(self): - if key.startswith("_"): - continue - value = getattr(self, key) - if not callable(value): - members.append(f"{key}={repr(value)}") - out += ", ".join(members) - out += ")" - return out - - def _ms(self, microservice: str, endpoint: List[str], **data) -> dict: - return self._client.ms(microservice, endpoint, **data) - - def _update(self, data: Union["GameObject", dict]): - self._data.update(data if isinstance(data, dict) else data._data) - - def update(self): - pass - - def clone(self, cls: Type[T]) -> T: - return cls(self._client, self._data) diff --git a/PyCrypCli/game_objects/inventory_element.py b/PyCrypCli/game_objects/inventory_element.py deleted file mode 100644 index 1f2cb01..0000000 --- a/PyCrypCli/game_objects/inventory_element.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import List - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.game_object import GameObject - - -class InventoryElement(GameObject): - @property - def uuid(self) -> str: - return self._data.get("element_uuid") - - @property - def name(self) -> str: - return self._data.get("element_name") - - @property - def related_ms(self) -> str: - return self._data.get("related_ms") - - @property - def owner(self) -> str: - return self._data.get("owner") - - @staticmethod - def list_inventory(client: Client) -> List["InventoryElement"]: - return [ - InventoryElement(client, element) for element in client.ms("inventory", ["inventory", "list"])["elements"] - ] - - def trade(self, target: str): - self._ms("inventory", ["inventory", "trade"], element_uuid=self.uuid, target=target) diff --git a/PyCrypCli/game_objects/network/__init__.py b/PyCrypCli/game_objects/network/__init__.py deleted file mode 100644 index 7b12e16..0000000 --- a/PyCrypCli/game_objects/network/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from PyCrypCli.game_objects.network.network import Network -from PyCrypCli.game_objects.network.network_invitation import NetworkInvitation -from PyCrypCli.game_objects.network.network_membership import NetworkMembership - -__all__ = ["Network", "NetworkInvitation", "NetworkMembership"] diff --git a/PyCrypCli/game_objects/network/network.py b/PyCrypCli/game_objects/network/network.py deleted file mode 100644 index 6d8ff5b..0000000 --- a/PyCrypCli/game_objects/network/network.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import List, TYPE_CHECKING - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.game_object import GameObject -from PyCrypCli.game_objects.network.network_invitation import NetworkInvitation -from PyCrypCli.game_objects.network.network_membership import NetworkMembership - -if TYPE_CHECKING: - from PyCrypCli.game_objects.device import Device - - -class Network(GameObject): - @property - def uuid(self) -> str: - return self._data.get("uuid") - - @property - def hidden(self) -> bool: - return self._data.get("hidden") - - @property - def owner(self) -> str: - return self._data.get("owner") - - @property - def name(self) -> str: - return self._data.get("name") - - @staticmethod - def get_public_networks(client: Client) -> List["Network"]: - return [Network(client, net) for net in client.ms("network", ["public"])["networks"]] - - @staticmethod - def get_by_uuid(client: Client, uuid: str) -> "Network": - return Network(client, client.ms("network", ["get"], uuid=uuid)) - - @staticmethod - def get_network_by_name(client: Client, name: str) -> "Network": - return Network(client, client.ms("network", ["name"], name=name)) - - def get_members(self) -> List[NetworkMembership]: - return [ - NetworkMembership(self._client, member) - for member in self._ms("network", ["members"], uuid=self.uuid)["members"] - ] - - def request_membership(self, device: "Device") -> NetworkInvitation: - return NetworkInvitation(self._client, self._ms("network", ["request"], uuid=self.uuid, device=device.uuid)) - - def get_membership_requests(self) -> List[NetworkInvitation]: - return [ - NetworkInvitation(self._client, invitation) - for invitation in self._ms("network", ["requests"], uuid=self.uuid)["requests"] - ] - - def invite_device(self, device: "Device") -> NetworkInvitation: - return NetworkInvitation(self._client, self._ms("network", ["invite"], uuid=self.uuid, device=device.uuid)) - - def leave(self, device: "Device"): - self._ms("network", ["leave"], uuid=self.uuid, device=device.uuid) - - def kick(self, device: "Device"): - self._ms("network", ["kick"], uuid=self.uuid, device=device.uuid) - - def delete(self): - self._ms("network", ["delete"], uuid=self.uuid) diff --git a/PyCrypCli/game_objects/network/network_invitation.py b/PyCrypCli/game_objects/network/network_invitation.py deleted file mode 100644 index e845d75..0000000 --- a/PyCrypCli/game_objects/network/network_invitation.py +++ /dev/null @@ -1,25 +0,0 @@ -from PyCrypCli.game_objects.game_object import GameObject - - -class NetworkInvitation(GameObject): - @property - def uuid(self) -> str: - return self._data.get("uuid") - - @property - def network(self) -> str: - return self._data.get("network") - - @property - def device(self) -> str: - return self._data.get("device") - - @property - def request(self) -> bool: - return self._data.get("request") - - def accept(self): - self._ms("network", ["accept"], uuid=self.uuid) - - def deny(self): - self._ms("network", ["deny"], uuid=self.uuid) diff --git a/PyCrypCli/game_objects/network/network_membership.py b/PyCrypCli/game_objects/network/network_membership.py deleted file mode 100644 index db24098..0000000 --- a/PyCrypCli/game_objects/network/network_membership.py +++ /dev/null @@ -1,15 +0,0 @@ -from PyCrypCli.game_objects.game_object import GameObject - - -class NetworkMembership(GameObject): - @property - def uuid(self) -> str: - return self._data.get("uuid") - - @property - def network(self) -> str: - return self._data.get("network") - - @property - def device(self) -> str: - return self._data.get("device") diff --git a/PyCrypCli/game_objects/resource_usage.py b/PyCrypCli/game_objects/resource_usage.py deleted file mode 100644 index b00e763..0000000 --- a/PyCrypCli/game_objects/resource_usage.py +++ /dev/null @@ -1,23 +0,0 @@ -from PyCrypCli.game_objects.game_object import GameObject - - -class ResourceUsage(GameObject): - @property - def cpu(self) -> float: - return min(1, self._data["usage_cpu"] / self._data["performance_cpu"]) - - @property - def ram(self) -> float: - return min(1, self._data["usage_ram"] / self._data["performance_ram"]) - - @property - def gpu(self) -> float: - return min(1, self._data["usage_gpu"] / self._data["performance_gpu"]) - - @property - def disk(self) -> float: - return min(1, self._data["usage_disk"] / self._data["performance_disk"]) - - @property - def network(self) -> float: - return min(1, self._data["usage_network"] / self._data["performance_network"]) diff --git a/PyCrypCli/game_objects/service/__init__.py b/PyCrypCli/game_objects/service/__init__.py deleted file mode 100644 index b3f9249..0000000 --- a/PyCrypCli/game_objects/service/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from PyCrypCli.game_objects.service.public_service import PublicService -from PyCrypCli.game_objects.service.service import Service -from PyCrypCli.game_objects.service.bruteforce_service import BruteforceService -from PyCrypCli.game_objects.service.miner import Miner -from PyCrypCli.game_objects.service.portscan_service import PortscanService - -__all__ = ["PublicService", "Service", "BruteforceService", "Miner", "PortscanService"] diff --git a/PyCrypCli/game_objects/service/bruteforce_service.py b/PyCrypCli/game_objects/service/bruteforce_service.py deleted file mode 100644 index f4312ab..0000000 --- a/PyCrypCli/game_objects/service/bruteforce_service.py +++ /dev/null @@ -1,60 +0,0 @@ -from datetime import datetime -from typing import Optional, Tuple - -from PyCrypCli.client import Client -from PyCrypCli.exceptions import AttackNotRunningException -from PyCrypCli.game_objects.service.service import Service - - -class BruteforceService(Service): - @property - def target_device(self) -> Optional[str]: - return self._data.get("target_device") - - @property - def target_service(self) -> Optional[str]: - return self._data.get("target_service") - - @property - def started(self) -> Optional[datetime]: - timestamp: Optional[int] = self._data.get("started") - if timestamp is None: - return None - - return datetime.fromtimestamp(timestamp) - - @property - def progress(self) -> Optional[float]: - return self._data.get("progress") - - @staticmethod - def get_bruteforce_service(client: Client, device_uuid: str) -> "BruteforceService": - service: BruteforceService = Service.get_service_by_name(client, device_uuid, "bruteforce").clone( - BruteforceService, - ) - service._update(service.get_bruteforce_details()) - return service - - def get_bruteforce_details(self) -> dict: - try: - return self._ms("service", ["bruteforce", "status"], device_uuid=self.device, service_uuid=self.uuid) - except AttackNotRunningException: - return {} - - def update(self): - self._update(BruteforceService.get_bruteforce_service(self._client, self.device)) - - def attack(self, target_device: str, target_service: str): - self._ms( - "service", - ["bruteforce", "attack"], - device_uuid=self.device, - service_uuid=self.uuid, - target_device=target_device, - target_service=target_service, - ) - - def stop(self) -> Tuple[bool, float, str]: - result = self._ms("service", ["bruteforce", "stop"], device_uuid=self.device, service_uuid=self.uuid) - self.update() - return result["access"], result["progress"], result["target_device"] diff --git a/PyCrypCli/game_objects/service/miner.py b/PyCrypCli/game_objects/service/miner.py deleted file mode 100644 index 7c60d38..0000000 --- a/PyCrypCli/game_objects/service/miner.py +++ /dev/null @@ -1,50 +0,0 @@ -from datetime import datetime -from typing import Optional, List - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.service.service import Service - - -class Miner(Service): - @property - def wallet(self) -> Optional[str]: - return self._data.get("wallet") - - @property - def started(self) -> Optional[datetime]: - timestamp: Optional[int] = self._data.get("started") - if timestamp is None: - return None - - return datetime.fromtimestamp(timestamp / 1000) - - @property - def power(self) -> Optional[float]: - return self._data.get("power") - - @staticmethod - def get_miner(client: Client, device_uuid: str) -> "Miner": - miner: Miner = Service.get_service_by_name(client, device_uuid, "miner").clone(Miner) - miner._update(miner.get_miner_details()) - return miner - - @staticmethod - def get_miners(client: Client, wallet_uuid: str) -> List["Miner"]: - return [ - Miner(client, {**miner["service"], **miner["miner"]}) - for miner in client.ms("service", ["miner", "list"], wallet_uuid=wallet_uuid)["miners"] - ] - - def get_miner_details(self) -> dict: - return self._ms("service", ["miner", "get"], service_uuid=self.uuid) - - def update(self): - self._update(Miner.get_miner(self._client, self.device)) - - def set_power(self, power: float): - self._ms("service", ["miner", "power"], service_uuid=self.uuid, power=power) - self.update() - - def set_wallet(self, wallet_uuid: str): - self._ms("service", ["miner", "wallet"], service_uuid=self.uuid, wallet_uuid=wallet_uuid) - self.update() diff --git a/PyCrypCli/game_objects/service/portscan_service.py b/PyCrypCli/game_objects/service/portscan_service.py deleted file mode 100644 index a8c1016..0000000 --- a/PyCrypCli/game_objects/service/portscan_service.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import List - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.service.public_service import PublicService -from PyCrypCli.game_objects.service.service import Service - - -class PortscanService(Service): - @staticmethod - def get_portscan_service(client: Client, device_uuid: str) -> "PortscanService": - return Service.get_service_by_name(client, device_uuid, "portscan").clone(PortscanService) - - def update(self): - self._update(PortscanService.get_portscan_service(self._client, self.device)) - - def scan(self, target: str) -> List[PublicService]: - return [PublicService(self._client, service) for service in self.use(target_device=target)["services"]] diff --git a/PyCrypCli/game_objects/service/public_service.py b/PyCrypCli/game_objects/service/public_service.py deleted file mode 100644 index 57595c9..0000000 --- a/PyCrypCli/game_objects/service/public_service.py +++ /dev/null @@ -1,30 +0,0 @@ -from PyCrypCli.client import Client -from PyCrypCli.game_objects.game_object import GameObject - - -class PublicService(GameObject): - @property - def uuid(self) -> str: - return self._data.get("uuid") - - @property - def device(self) -> str: - return self._data.get("device") - - @property - def name(self) -> str: - return self._data.get("name") - - @property - def running_port(self) -> int: - return self._data.get("running_port") - - @staticmethod - def get_public_service(client: Client, device_uuid: str, service_uuid: str) -> "PublicService": - return PublicService( - client, - client.ms("service", ["public_info"], device_uuid=device_uuid, service_uuid=service_uuid), - ) - - def update(self): - self._update(PublicService.get_public_service(self._client, self.device, self.uuid)) diff --git a/PyCrypCli/game_objects/service/service.py b/PyCrypCli/game_objects/service/service.py deleted file mode 100644 index ca17700..0000000 --- a/PyCrypCli/game_objects/service/service.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import List - -from PyCrypCli.exceptions import ServiceNotFoundException - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.service.public_service import PublicService - - -class Service(PublicService): - @property - def owner(self) -> str: - return self._data.get("owner") - - @property - def running(self) -> bool: - return self._data.get("running") - - @property - def part_owner(self) -> str: - return self._data.get("part_owner") - - @property - def speed(self) -> float: - return self._data.get("speed") - - @staticmethod - def get_services(client: Client, device_uuid: str) -> List["Service"]: - return [ - Service(client, service) for service in client.ms("service", ["list"], device_uuid=device_uuid)["services"] - ] - - @staticmethod - def get_service(client: Client, device_uuid: str, service_uuid: str) -> "Service": - return Service( - client, - client.ms("service", ["private_info"], device_uuid=device_uuid, service_uuid=service_uuid), - ) - - @staticmethod - def get_service_by_name(client: Client, device_uuid: str, name: str) -> "Service": - for service in Service.get_services(client, device_uuid): - if service.name == name: - return service - raise ServiceNotFoundException - - @staticmethod - def list_part_owner(client: Client) -> List["Service"]: - return [Service(client, service) for service in client.ms("service", ["list_part_owner"])["services"]] - - def update(self): - self._update(Service.get_service(self._client, self.device, self.uuid)) - - def use(self, **data) -> dict: - return self._ms("service", ["use"], device_uuid=self.device, service_uuid=self.uuid, **data) - - def toggle(self): - self._update(self._ms("service", ["toggle"], device_uuid=self.device, service_uuid=self.uuid)) - - def delete(self): - self._ms("service", ["delete"], device_uuid=self.device, service_uuid=self.uuid) diff --git a/PyCrypCli/game_objects/shop/__init__.py b/PyCrypCli/game_objects/shop/__init__.py deleted file mode 100644 index 46b7bc0..0000000 --- a/PyCrypCli/game_objects/shop/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from PyCrypCli.game_objects.shop.shop_category import ShopCategory -from PyCrypCli.game_objects.shop.shop_product import ShopProduct - - -__all__ = ["ShopProduct", "ShopCategory"] diff --git a/PyCrypCli/game_objects/shop/shop_category.py b/PyCrypCli/game_objects/shop/shop_category.py deleted file mode 100644 index a41118d..0000000 --- a/PyCrypCli/game_objects/shop/shop_category.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.game_object import GameObject -from PyCrypCli.game_objects.shop.shop_product import ShopProduct - - -class ShopCategory(GameObject): - @property - def name(self) -> str: - return self._data.get("name") - - @property - def index(self) -> int: - return self._data.get("index") - - @property - def items(self) -> List[ShopProduct]: - return [ShopProduct(self._client, {"name": k, **v}) for k, v in self._data.get("items").items()] - - @property - def subcategories(self) -> List["ShopCategory"]: - out = [ShopCategory(self._client, {"name": k, **v}) for k, v in self._data.get("categories").items()] - out.sort(key=lambda c: c.index) - return out - - @staticmethod - def shop_list(client: Client) -> List["ShopCategory"]: - out = [ - ShopCategory(client, {"name": k, **v}) - for k, v in client.ms("inventory", ["shop", "list"])["categories"].items() - ] - out.sort(key=lambda c: c.index) - return out diff --git a/PyCrypCli/game_objects/shop/shop_product.py b/PyCrypCli/game_objects/shop/shop_product.py deleted file mode 100644 index 4ca5f07..0000000 --- a/PyCrypCli/game_objects/shop/shop_product.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Dict, List - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.game_object import GameObject -from PyCrypCli.game_objects.inventory_element import InventoryElement -from PyCrypCli.game_objects.wallet import Wallet - - -class ShopProduct(GameObject): - @property - def name(self) -> str: - return self._data.get("name") - - @property - def price(self) -> int: - return self._data.get("price") - - @property - def related_ms(self) -> str: - return self._data.get("related_ms") - - @staticmethod - def shop_info(client: Client, product: str) -> "ShopProduct": - return ShopProduct(client, client.ms("inventory", ["shop", "info"], product=product)) - - @staticmethod - def bulk_buy(client: Client, products: Dict["ShopProduct", int], wallet: Wallet) -> List[InventoryElement]: - return [ - InventoryElement(client, element) - for element in client.ms( - "inventory", - ["shop", "buy"], - products={k.name: v for k, v in products.items()}, - wallet_uuid=wallet.uuid, - key=wallet.key, - )["bought_products"] - ] - - def buy(self, wallet: Wallet) -> InventoryElement: - return ShopProduct.bulk_buy(self._client, {self: 1}, wallet)[0] diff --git a/PyCrypCli/game_objects/transaction.py b/PyCrypCli/game_objects/transaction.py deleted file mode 100644 index 31ac81f..0000000 --- a/PyCrypCli/game_objects/transaction.py +++ /dev/null @@ -1,30 +0,0 @@ -from datetime import datetime - -from PyCrypCli.game_objects.game_object import GameObject -from PyCrypCli.util import convert_timestamp - - -class Transaction(GameObject): - @property - def timestamp(self) -> datetime: - return convert_timestamp(datetime.fromisoformat(self._data.get("time_stamp"))) - - @property - def source_uuid(self) -> str: - return self._data.get("source_uuid") - - @property - def destination_uuid(self) -> str: - return self._data.get("destination_uuid") - - @property - def amount(self) -> int: - return self._data.get("send_amount") - - @property - def usage(self) -> str: - return self._data.get("usage") - - @property - def origin(self) -> int: - return self._data.get("origin") diff --git a/PyCrypCli/game_objects/wallet/__init__.py b/PyCrypCli/game_objects/wallet/__init__.py deleted file mode 100644 index d182936..0000000 --- a/PyCrypCli/game_objects/wallet/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from PyCrypCli.game_objects.wallet.public_wallet import PublicWallet -from PyCrypCli.game_objects.wallet.wallet import Wallet - - -__all__ = ["Wallet", "PublicWallet"] diff --git a/PyCrypCli/game_objects/wallet/public_wallet.py b/PyCrypCli/game_objects/wallet/public_wallet.py deleted file mode 100644 index ce33d0d..0000000 --- a/PyCrypCli/game_objects/wallet/public_wallet.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import List - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.game_object import GameObject - - -class PublicWallet(GameObject): - @property - def uuid(self) -> str: - return self._data.get("source_uuid") - - @staticmethod - def get_public_wallet(client: Client, uuid: str) -> "PublicWallet": - return PublicWallet(client, {"source_uuid": uuid}) - - @staticmethod - def list_wallets(client: Client) -> List["PublicWallet"]: - return [PublicWallet.get_public_wallet(client, uuid) for uuid in client.ms("currency", ["list"])["wallets"]] - - def reset_wallet(self): - self._ms("currency", ["reset"], source_uuid=self.uuid) diff --git a/PyCrypCli/game_objects/wallet/wallet.py b/PyCrypCli/game_objects/wallet/wallet.py deleted file mode 100644 index b409a35..0000000 --- a/PyCrypCli/game_objects/wallet/wallet.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import List - -from PyCrypCli.client import Client -from PyCrypCli.game_objects.transaction import Transaction -from PyCrypCli.game_objects.wallet.public_wallet import PublicWallet -from PyCrypCli.game_objects.service import Miner - - -class Wallet(PublicWallet): - @property - def key(self) -> str: - return self._data.get("key") - - @property - def user(self) -> str: - return self._data.get("user_uuid") - - @property - def amount(self) -> int: - return self._data.get("amount") - - @property - def transactions(self) -> int: - return self._data.get("transactions") - - @staticmethod - def create_wallet(client: Client) -> "Wallet": - return Wallet(client, client.ms("currency", ["create"])) - - @staticmethod - def get_wallet(client: Client, uuid: str, key: str) -> "Wallet": - return Wallet(client, client.ms("currency", ["get"], source_uuid=uuid, key=key)) - - def update(self): - self._update(Wallet.get_wallet(self._client, self.uuid, self.key)) - - def get_transactions(self, count: int, offset: int) -> List[Transaction]: - return [ - Transaction(self._client, t) - for t in self._ms( - "currency", - ["transactions"], - source_uuid=self.uuid, - key=self.key, - count=count, - offset=offset, - )["transactions"] - ] - - def get_miners(self) -> List[Miner]: - return Miner.get_miners(self._client, self.uuid) - - def get_mining_rate(self) -> float: - return sum(miner.speed for miner in self.get_miners() if miner.running) - - def send(self, destination: PublicWallet, amount: int, usage: str): - self._ms( - "currency", - ["send"], - source_uuid=self.uuid, - key=self.key, - send_amount=amount, - destination_uuid=destination.uuid, - usage=usage, - ) - self.update() - - def delete(self): - self._ms("currency", ["delete"], source_uuid=self.uuid, key=self.key) diff --git a/PyCrypCli/models/__init__.py b/PyCrypCli/models/__init__.py new file mode 100644 index 0000000..2fb4f11 --- /dev/null +++ b/PyCrypCli/models/__init__.py @@ -0,0 +1,41 @@ +from .config import Config, ServerConfig +from .device import Device +from .device_hardware import DeviceHardware +from .file import File +from .hardware_config import HardwareConfig +from .inventory_element import InventoryElement +from .model import Model +from .network import Network, NetworkInvitation, NetworkMembership +from .resource_usage import ResourceUsage +from .server_responses import StatusResponse, InfoResponse, TokenResponse +from .service import Service, PublicService, Miner, BruteforceService, PortscanService +from .shop import ShopProduct, ShopCategory +from .wallet import Wallet, PublicWallet, Transaction + +__all__ = [ + "Config", + "ServerConfig", + "Device", + "DeviceHardware", + "File", + "HardwareConfig", + "InventoryElement", + "Model", + "Network", + "NetworkInvitation", + "NetworkMembership", + "ResourceUsage", + "StatusResponse", + "InfoResponse", + "TokenResponse", + "Service", + "PublicService", + "Miner", + "BruteforceService", + "PortscanService", + "ShopProduct", + "ShopCategory", + "Wallet", + "PublicWallet", + "Transaction", +] diff --git a/PyCrypCli/models/config.py b/PyCrypCli/models/config.py new file mode 100644 index 0000000..3ef61ab --- /dev/null +++ b/PyCrypCli/models/config.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class ServerConfig(BaseModel): + token: str | None + + +class Config(BaseModel): + servers: dict[str, ServerConfig] + + @staticmethod + def get_default_config() -> Config: + return Config(servers={}) diff --git a/PyCrypCli/models/device.py b/PyCrypCli/models/device.py new file mode 100644 index 0000000..30074ee --- /dev/null +++ b/PyCrypCli/models/device.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from typing import Any, cast, TYPE_CHECKING + +from pydantic import Field + +from .device_hardware import DeviceHardware +from .file import File +from .model import Model +from .network import Network, NetworkInvitation +from .resource_usage import ResourceUsage +from .service import PublicService, Service, Miner + +if TYPE_CHECKING: + from ..client import Client + + +class Device(Model): + uuid: str + name: str + owner_uuid: str = Field(alias="owner") + powered_on: bool + + def __hash__(self) -> int: + return hash(self.uuid) + + @staticmethod + def get_device(client: Client, device_uuid: str) -> Device: + return Device.parse(client, client.ms("device", ["device", "info"], device_uuid=device_uuid)) + + @staticmethod + def list_devices(client: Client) -> list[Device]: + return [Device.parse(client, device) for device in client.ms("device", ["device", "all"])["devices"]] + + def update(self) -> Device: + return self._update(Device.get_device(self._client, self.uuid)) + + @staticmethod + def build(client: Client, mainboard: str, cpu: str, gpu: str, ram: list[str], disk: list[str]) -> Device: + return Device.parse( + client, + client.ms("device", ["device", "create"], motherboard=mainboard, cpu=cpu, gpu=gpu, ram=ram, disk=disk), + ) + + @staticmethod + def starter_device(client: Client) -> Device: + return Device.parse(client, client.ms("device", ["device", "starter_device"])) + + @staticmethod + def spot(client: Client) -> Device: + return Device.parse(client, client.ms("device", ["device", "spot"])) + + def power(self) -> Device: + return self._update(self._ms("device", ["device", "power"], device_uuid=self.uuid)) + + def change_name(self, name: str) -> Device: + return self._update(self._ms("device", ["device", "change_name"], device_uuid=self.uuid, name=name)) + + def delete(self) -> None: + self._ms("device", ["device", "delete"], device_uuid=self.uuid) + + def get_files(self, parent_dir_uuid: str | None) -> list[File]: + return [ + File.parse(self._client, file) + for file in self._ms("device", ["file", "all"], device_uuid=self.uuid, parent_dir_uuid=parent_dir_uuid)[ + "files" + ] + ] + + def get_file(self, file_uuid: str) -> File: + return File.get_file(self._client, self.uuid, file_uuid) + + def create_file(self, filename: str, content: str, is_directory: bool, parent_dir_uuid: str | None) -> File: + return File.parse( + self._client, + self._ms( + "device", + ["file", "create"], + device_uuid=self.uuid, + filename=filename, + content=content, + is_directory=is_directory, + parent_dir_uuid=parent_dir_uuid, + ), + ) + + def get_public_service(self, service_uuid: str) -> PublicService: + return PublicService.get_public_service(self._client, self.uuid, service_uuid) + + def get_services(self) -> list[Service]: + return Service.get_services(self._client, self.uuid) + + def get_service(self, service_uuid: str) -> Service: + return Service.get_service(self._client, self.uuid, service_uuid) + + def get_service_by_name(self, service: str) -> Service: + return Service.get_service_by_name(self._client, self.uuid, service) + + def get_miner(self) -> Miner: + return Miner.get_miner(self._client, self.uuid) + + def create_service(self, name: str, **extra: Any) -> Service: + return Service.parse(self._client, self._ms("service", ["create"], name=name, device_uuid=self.uuid, **extra)) + + def part_owner(self) -> bool: + return cast(bool, self._ms("service", ["part_owner"], device_uuid=self.uuid)["ok"]) + + def get_hardware(self) -> list[DeviceHardware]: + return [ + DeviceHardware.parse(self._client, dh) + for dh in self._ms("device", ["device", "info"], device_uuid=self.uuid)["hardware"] + ] + + def get_resource_usage(self) -> ResourceUsage: + return ResourceUsage.parse(self._client, self._ms("device", ["hardware", "resources"], device_uuid=self.uuid)) + + def get_networks(self) -> list[Network]: + return [ + Network.parse(self._client, net) for net in self._ms("network", ["member"], device=self.uuid)["networks"] + ] + + def create_network(self, name: str, hidden: bool) -> Network: + return Network.parse(self._client, self._ms("network", ["create"], device=self.uuid, name=name, hidden=hidden)) + + def get_network_invitations(self) -> list[NetworkInvitation]: + return [ + NetworkInvitation.parse(self._client, invitation) + for invitation in self._ms("network", ["invitations"], device=self.uuid)["invitations"] + ] diff --git a/PyCrypCli/models/device_hardware.py b/PyCrypCli/models/device_hardware.py new file mode 100644 index 0000000..6b2f5a8 --- /dev/null +++ b/PyCrypCli/models/device_hardware.py @@ -0,0 +1,8 @@ +from .model import Model + + +class DeviceHardware(Model): + uuid: str + device_uuid: str + hardware_element: str + hardware_type: str diff --git a/PyCrypCli/models/file.py b/PyCrypCli/models/file.py new file mode 100644 index 0000000..d2720e6 --- /dev/null +++ b/PyCrypCli/models/file.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from .model import Model + +if TYPE_CHECKING: + from ..client import Client + + +class File(Model): + uuid: str | None + device_uuid: str = Field(alias="device") + name: str = Field(alias="filename") + content: str + is_directory: bool + parent_dir_uuid: str | None + + @property + def is_root_directory(self) -> bool: + return self.uuid is None + + @staticmethod + def get_root_directory(client: Client, device_uuid: str) -> File: + return File.parse( + client, + { + "uuid": None, + "device": device_uuid, + "filename": "", + "content": "", + "is_directory": True, + "parent_dir_uuid": None, + }, + ) + + @staticmethod + def get_file(client: Client, device_uuid: str, file_uuid: str) -> File: + return File.parse(client, client.ms("device", ["file", "info"], device_uuid=device_uuid, file_uuid=file_uuid)) + + def update(self) -> File: + if not self.uuid: + return self + + return self._update(File.get_file(self._client, self.device_uuid, self.uuid)) + + def move(self, new_filename: str, new_parent_dir_uuid: str | None) -> File: + return self._update( + self._ms( + "device", + ["file", "move"], + device_uuid=self.device_uuid, + file_uuid=self.uuid, + new_filename=new_filename, + new_parent_dir_uuid=new_parent_dir_uuid, + ) + ) + + def edit(self, new_content: str) -> File: + return self._update( + self._ms( + "device", ["file", "update"], device_uuid=self.device_uuid, file_uuid=self.uuid, content=new_content + ) + ) + + def delete(self) -> None: + self._ms("device", ["file", "delete"], device_uuid=self.device_uuid, file_uuid=self.uuid) diff --git a/PyCrypCli/models/hardware_config/__init__.py b/PyCrypCli/models/hardware_config/__init__.py new file mode 100644 index 0000000..13988a3 --- /dev/null +++ b/PyCrypCli/models/hardware_config/__init__.py @@ -0,0 +1,22 @@ +from .case import Case +from .cpu import CPU +from .disk import Disk +from .gpu import GPU +from .hardware_config import HardwareConfig, StartPC +from .mainboard import Mainboard +from .power_pack import PowerPack +from .processor_cooler import ProcessorCooler +from .ram import RAM + +__all__ = [ + "Case", + "CPU", + "Disk", + "GPU", + "HardwareConfig", + "Mainboard", + "PowerPack", + "ProcessorCooler", + "RAM", + "StartPC", +] diff --git a/PyCrypCli/models/hardware_config/case.py b/PyCrypCli/models/hardware_config/case.py new file mode 100644 index 0000000..85e7987 --- /dev/null +++ b/PyCrypCli/models/hardware_config/case.py @@ -0,0 +1,8 @@ +from typing import Literal + +from ..model import Model + + +class Case(Model): + id: int + size: Literal["small", "middle", "big"] diff --git a/PyCrypCli/models/hardware_config/cpu.py b/PyCrypCli/models/hardware_config/cpu.py new file mode 100644 index 0000000..6d2f538 --- /dev/null +++ b/PyCrypCli/models/hardware_config/cpu.py @@ -0,0 +1,17 @@ +from pydantic import Field + +from .mainboard import GraphicUnit +from ..model import Model + + +class CPU(Model): + id: int + frequency_min: int = Field(alias="frequencyMin") + frequency_max: int = Field(alias="frequencyMax") + socket: str + cores: int + turbo_speed: bool = Field(alias="turboSpeed") + overclock: bool = Field(alias="overClock") + max_temperature: int = Field(alias="maxTemperature") + graphic_unit: GraphicUnit | None = Field(alias="graphicUnit") + power: int diff --git a/PyCrypCli/models/hardware_config/disk.py b/PyCrypCli/models/hardware_config/disk.py new file mode 100644 index 0000000..68c36ad --- /dev/null +++ b/PyCrypCli/models/hardware_config/disk.py @@ -0,0 +1,14 @@ +from pydantic import Field + +from .mainboard import Interface +from ..model import Model + + +class Disk(Model): + id: int + type: str = Field(alias="diskTyp") + capacity: int + writing_speed: int = Field(alias="writingSpeed") + reading_speed: int = Field(alias="readingSpeed") + interface: Interface + power: int diff --git a/PyCrypCli/models/hardware_config/gpu.py b/PyCrypCli/models/hardware_config/gpu.py new file mode 100644 index 0000000..619ac8a --- /dev/null +++ b/PyCrypCli/models/hardware_config/gpu.py @@ -0,0 +1,13 @@ +from pydantic import Field + +from .mainboard import Interface +from ..model import Model + + +class GPU(Model): + id: int + ram_size: int = Field(alias="ramSize") + ram_type: Interface = Field(alias="ramTyp") + frequency: int + interface: Interface + power: int diff --git a/PyCrypCli/models/hardware_config/hardware_config.py b/PyCrypCli/models/hardware_config/hardware_config.py new file mode 100644 index 0000000..4a57e54 --- /dev/null +++ b/PyCrypCli/models/hardware_config/hardware_config.py @@ -0,0 +1,34 @@ +from pydantic import Field + +from .case import Case +from .cpu import CPU +from .disk import Disk +from .gpu import GPU +from .mainboard import Mainboard +from .power_pack import PowerPack +from .processor_cooler import ProcessorCooler +from .ram import RAM +from ..model import Model + + +class StartPC(Model): + mainboard: str + cpu: list[str] + processor_cooler: list[str] = Field(alias="processorCooler") + ram: list[str] + gpu: list[str] + disk: list[str] + power_pack: str = Field(alias="powerPack") + case: str + + +class HardwareConfig(Model): + start_pc: StartPC + mainboard: dict[str, Mainboard] + cpu: dict[str, CPU] + processor_cooler: dict[str, ProcessorCooler] = Field(alias="processorCooler") + ram: dict[str, RAM] + gpu: dict[str, GPU] + disk: dict[str, Disk] + power_pack: dict[str, PowerPack] = Field(alias="powerPack") + case: dict[str, Case] diff --git a/PyCrypCli/models/hardware_config/mainboard.py b/PyCrypCli/models/hardware_config/mainboard.py new file mode 100644 index 0000000..941ac3a --- /dev/null +++ b/PyCrypCli/models/hardware_config/mainboard.py @@ -0,0 +1,50 @@ +from typing import NamedTuple + +from pydantic import Field + +from ..model import Model + +Interface = NamedTuple("Interface", [("name", str), ("version", int)]) + + +class RAM(Model): + ram_slots: int = Field(alias="ramSlots") + max_ram_size: int = Field(alias="maxRamSize") + ram_type: list[Interface] = Field(alias="ramTyp") + frequency: list[int] + + +class GraphicUnit(Model): + name: str + ram_size: int = Field(alias="ramSize") + frequency: int + + +class ExpansionSlot(Model): + interface: Interface + interface_slots: int = Field(alias="interfaceSlots") + + +class DiskStorage(Model): + disk_slots: int = Field(alias="diskSlots") + interface: list[Interface] + + +class NetworkPort(Model): + name: str + interface: str + speed: int + + +class Mainboard(Model): + id: int + case: str + cpu_socket: str = Field(alias="cpuSocket") + cpu_slots: int = Field(alias="cpuSlots") + core_temperature_control: bool = Field(alias="coreTemperatureControl") + usb_ports: int = Field(alias="usbPorts") + graphic_unit_on_board: GraphicUnit = Field(alias="graphicUnitOnBoard") + expansion_slots: list[ExpansionSlot] = Field(alias="expansionSlots") + disk_storage: DiskStorage = Field(alias="diskStorage") + network_port: NetworkPort = Field(alias="NetworkPort") + power: int diff --git a/PyCrypCli/models/hardware_config/power_pack.py b/PyCrypCli/models/hardware_config/power_pack.py new file mode 100644 index 0000000..4b72045 --- /dev/null +++ b/PyCrypCli/models/hardware_config/power_pack.py @@ -0,0 +1,8 @@ +from pydantic import Field + +from ..model import Model + + +class PowerPack(Model): + id: int + total_power: int = Field(alias="totalPower") diff --git a/PyCrypCli/models/hardware_config/processor_cooler.py b/PyCrypCli/models/hardware_config/processor_cooler.py new file mode 100644 index 0000000..9d4bbdf --- /dev/null +++ b/PyCrypCli/models/hardware_config/processor_cooler.py @@ -0,0 +1,10 @@ +from pydantic import Field + +from ..model import Model + + +class ProcessorCooler(Model): + id: int + speed: int = Field(alias="coolerSpeed") + socket: str + power: int diff --git a/PyCrypCli/models/hardware_config/ram.py b/PyCrypCli/models/hardware_config/ram.py new file mode 100644 index 0000000..092e667 --- /dev/null +++ b/PyCrypCli/models/hardware_config/ram.py @@ -0,0 +1,12 @@ +from pydantic import Field + +from .mainboard import Interface +from ..model import Model + + +class RAM(Model): + id: int + size: int = Field(alias="ramSize") + type: Interface = Field(alias="ramTyp") + frequency: int + power: int diff --git a/PyCrypCli/models/inventory_element.py b/PyCrypCli/models/inventory_element.py new file mode 100644 index 0000000..81ecca9 --- /dev/null +++ b/PyCrypCli/models/inventory_element.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from .model import Model + +if TYPE_CHECKING: + from ..client import Client + + +class InventoryElement(Model): + uuid: str = Field(alias="element_uuid") + name: str = Field(alias="element_name") + related_ms: str + owner_uuid: str = Field(alias="owner") + + @staticmethod + def list_inventory(client: Client) -> list[InventoryElement]: + return [ + InventoryElement.parse(client, element) + for element in client.ms("inventory", ["inventory", "list"])["elements"] + ] + + def trade(self, target: str) -> None: + self._ms("inventory", ["inventory", "trade"], element_uuid=self.uuid, target=target) diff --git a/PyCrypCli/models/model.py b/PyCrypCli/models/model.py new file mode 100644 index 0000000..2a0572a --- /dev/null +++ b/PyCrypCli/models/model.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Type, TypeVar, Any, TYPE_CHECKING + +from pydantic import BaseModel, PrivateAttr, ValidationError + +if TYPE_CHECKING: + from ..client import Client + +ModelType = TypeVar("ModelType", bound="Model") + + +class Model(BaseModel): + _client: Client = PrivateAttr() + + @classmethod + def parse(cls: Type[ModelType], client: Client, obj: dict[Any, Any]) -> ModelType: + out = cls.parse_obj(obj) + out._client = client + return out + + def _ms(self, microservice: str, endpoint: list[str], **data: Any) -> dict[Any, Any]: + return self._client.ms(microservice, endpoint, **data) + + def _update(self: ModelType, obj: ModelType | dict[Any, Any]) -> ModelType: + if isinstance(obj, dict): + obj = self.validate(obj) + for k, v in obj.dict().items(): + setattr(self, k, v) + return self diff --git a/PyCrypCli/models/network/__init__.py b/PyCrypCli/models/network/__init__.py new file mode 100644 index 0000000..880a2e1 --- /dev/null +++ b/PyCrypCli/models/network/__init__.py @@ -0,0 +1,5 @@ +from .network import Network +from .network_invitation import NetworkInvitation +from .network_membership import NetworkMembership + +__all__ = ["Network", "NetworkInvitation", "NetworkMembership"] diff --git a/PyCrypCli/models/network/network.py b/PyCrypCli/models/network/network.py new file mode 100644 index 0000000..a1dc040 --- /dev/null +++ b/PyCrypCli/models/network/network.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from .network_invitation import NetworkInvitation +from .network_membership import NetworkMembership +from ..model import Model + +if TYPE_CHECKING: + from ..device import Device + from ...client import Client + + +class Network(Model): + uuid: str + hidden: bool + owner_uuid: str = Field(alias="owner") + name: str + + @staticmethod + def get_public_networks(client: Client) -> list[Network]: + return [Network.parse(client, net) for net in client.ms("network", ["public"])["networks"]] + + @staticmethod + def get_by_uuid(client: Client, uuid: str) -> Network: + return Network.parse(client, client.ms("network", ["get"], uuid=uuid)) + + @staticmethod + def get_network_by_name(client: Client, name: str) -> Network: + return Network.parse(client, client.ms("network", ["name"], name=name)) + + def get_members(self) -> list[NetworkMembership]: + return [ + NetworkMembership.parse(self._client, member) + for member in self._ms("network", ["members"], uuid=self.uuid)["members"] + ] + + def request_membership(self, device: Device) -> NetworkInvitation: + return NetworkInvitation.parse( + self._client, self._ms("network", ["request"], uuid=self.uuid, device=device.uuid) + ) + + def get_membership_requests(self) -> list[NetworkInvitation]: + return [ + NetworkInvitation.parse(self._client, invitation) + for invitation in self._ms("network", ["requests"], uuid=self.uuid)["requests"] + ] + + def invite_device(self, device: Device) -> NetworkInvitation: + return NetworkInvitation.parse( + self._client, self._ms("network", ["invite"], uuid=self.uuid, device=device.uuid) + ) + + def leave(self, device: Device) -> None: + self._ms("network", ["leave"], uuid=self.uuid, device=device.uuid) + + def kick(self, device: Device) -> None: + self._ms("network", ["kick"], uuid=self.uuid, device=device.uuid) + + def delete(self) -> None: + self._ms("network", ["delete"], uuid=self.uuid) diff --git a/PyCrypCli/models/network/network_invitation.py b/PyCrypCli/models/network/network_invitation.py new file mode 100644 index 0000000..67fdfb1 --- /dev/null +++ b/PyCrypCli/models/network/network_invitation.py @@ -0,0 +1,16 @@ +from pydantic import Field + +from ..model import Model + + +class NetworkInvitation(Model): + uuid: str + network_uuid: str = Field(alias="network") + device_uuid: str = Field(alias="device") + request: bool + + def accept(self) -> None: + self._ms("network", ["accept"], uuid=self.uuid) + + def deny(self) -> None: + self._ms("network", ["deny"], uuid=self.uuid) diff --git a/PyCrypCli/models/network/network_membership.py b/PyCrypCli/models/network/network_membership.py new file mode 100644 index 0000000..660e446 --- /dev/null +++ b/PyCrypCli/models/network/network_membership.py @@ -0,0 +1,9 @@ +from pydantic import Field + +from ..model import Model + + +class NetworkMembership(Model): + uuid: str + network_uuid: str = Field(alias="network") + device_uuid: str = Field(alias="device") diff --git a/PyCrypCli/models/resource_usage.py b/PyCrypCli/models/resource_usage.py new file mode 100644 index 0000000..1e6582c --- /dev/null +++ b/PyCrypCli/models/resource_usage.py @@ -0,0 +1,34 @@ +from .model import Model + + +class ResourceUsage(Model): + usage_cpu: float + usage_ram: float + usage_gpu: float + usage_disk: float + usage_network: float + performance_cpu: float + performance_ram: float + performance_gpu: float + performance_disk: float + performance_network: float + + @property + def cpu(self) -> float: + return min(self.usage_cpu / self.performance_cpu, 1) + + @property + def ram(self) -> float: + return min(self.usage_ram / self.performance_ram, 1) + + @property + def gpu(self) -> float: + return min(self.usage_gpu / self.performance_gpu, 1) + + @property + def disk(self) -> float: + return min(self.usage_disk / self.performance_disk, 1) + + @property + def network(self) -> float: + return min(self.usage_network / self.performance_network, 1) diff --git a/PyCrypCli/models/server_responses.py b/PyCrypCli/models/server_responses.py new file mode 100644 index 0000000..839cadb --- /dev/null +++ b/PyCrypCli/models/server_responses.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from pydantic import Field + +from .model import Model + + +class StatusResponse(Model): + online: int + + +class InfoResponse(Model): + name: str + uuid: str + created: datetime + last_login: datetime = Field(alias="last") + online: int + + +class TokenResponse(Model): + token: str diff --git a/PyCrypCli/models/service/__init__.py b/PyCrypCli/models/service/__init__.py new file mode 100644 index 0000000..cdb6e5e --- /dev/null +++ b/PyCrypCli/models/service/__init__.py @@ -0,0 +1,8 @@ +from .bruteforce_service import BruteforceService +from .miner import Miner +from .portscan_service import PortscanService +from .public_service import PublicService +from .service import Service + + +__all__ = ["BruteforceService", "Miner", "PortscanService", "PublicService", "Service"] diff --git a/PyCrypCli/models/service/bruteforce_service.py b/PyCrypCli/models/service/bruteforce_service.py new file mode 100644 index 0000000..996d65d --- /dev/null +++ b/PyCrypCli/models/service/bruteforce_service.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, TYPE_CHECKING + +from pydantic import Field + +from .service import Service +from ...exceptions import AttackNotRunningError + +if TYPE_CHECKING: + from ...client import Client + + +class BruteforceService(Service): + target_device_uuid: str | None = Field(alias="target_device") + target_service_uuid: str | None = Field(alias="target_service") + started: datetime | None + progress: float | None + + @staticmethod + def get_bruteforce_service(client: Client, device_uuid: str) -> BruteforceService: + service: Service = Service.get_service_by_name(client, device_uuid, "bruteforce") + return BruteforceService.parse( + client, + service.dict(by_alias=True) | BruteforceService.get_bruteforce_details(client, device_uuid, service.uuid), + ) + + @staticmethod + def get_bruteforce_details(client: Client, device_uuid: str, service_uuid: str) -> dict[Any, Any]: + try: + return client.ms("service", ["bruteforce", "status"], device_uuid=device_uuid, service_uuid=service_uuid) + except AttackNotRunningError: + return {"target_device_uuid": None, "target_service_uuid": None, "started": None, "progress": None} + + def update(self) -> BruteforceService: + return self._update(BruteforceService.get_bruteforce_service(self._client, self.device_uuid)) + + def attack(self, target_device: str, target_service: str) -> None: + self._ms( + "service", + ["bruteforce", "attack"], + device_uuid=self.device_uuid, + service_uuid=self.uuid, + target_device=target_device, + target_service=target_service, + ) + + def stop(self) -> tuple[bool, float, str]: + result = self._ms("service", ["bruteforce", "stop"], device_uuid=self.device_uuid, service_uuid=self.uuid) + self.update() + return result["access"], result["progress"], result["target_device"] diff --git a/PyCrypCli/models/service/miner.py b/PyCrypCli/models/service/miner.py new file mode 100644 index 0000000..095faf1 --- /dev/null +++ b/PyCrypCli/models/service/miner.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, TYPE_CHECKING + +from pydantic import Field + +from .service import Service + +if TYPE_CHECKING: + from ...client import Client + + +class Miner(Service): + wallet_uuid: str = Field(alias="wallet") + started: datetime | None + power: float + + @staticmethod + def get_miner(client: Client, device_uuid: str) -> Miner: + service: Service = Service.get_service_by_name(client, device_uuid, "miner") + return Miner.parse(client, service.dict(by_alias=True) | Miner.get_miner_details(client, service.uuid)) + + @staticmethod + def get_miners(client: Client, wallet_uuid: str) -> list[Miner]: + return [ + Miner.parse(client, {**miner["service"], **miner["miner"]}) + for miner in client.ms("service", ["miner", "list"], retry=5, wallet_uuid=wallet_uuid)["miners"] + ] + + @staticmethod + def get_miner_details(client: Client, service_uuid: str) -> dict[Any, Any]: + return client.ms("service", ["miner", "get"], service_uuid=service_uuid) + + def update(self) -> Miner: + return self._update(Miner.get_miner(self._client, self.device_uuid)) + + def set_power(self, power: float) -> Miner: + self._ms("service", ["miner", "power"], service_uuid=self.uuid, power=power) + return self.update() + + def set_wallet(self, wallet_uuid: str) -> Miner: + self._ms("service", ["miner", "wallet"], service_uuid=self.uuid, wallet_uuid=wallet_uuid) + return self.update() diff --git a/PyCrypCli/models/service/portscan_service.py b/PyCrypCli/models/service/portscan_service.py new file mode 100644 index 0000000..d6d9762 --- /dev/null +++ b/PyCrypCli/models/service/portscan_service.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .public_service import PublicService +from .service import Service + +if TYPE_CHECKING: + from ...client import Client + + +class PortscanService(Service): + @staticmethod + def get_portscan_service(client: Client, device_uuid: str) -> PortscanService: + service: Service = Service.get_service_by_name(client, device_uuid, "portscan") + return PortscanService.parse(client, service.dict(by_alias=True)) + + def update(self) -> PortscanService: + return self._update(PortscanService.get_portscan_service(self._client, self.device_uuid)) + + def scan(self, target: str) -> list[PublicService]: + return [PublicService.parse(self._client, service) for service in self.use(target_device=target)["services"]] diff --git a/PyCrypCli/models/service/public_service.py b/PyCrypCli/models/service/public_service.py new file mode 100644 index 0000000..b4afc91 --- /dev/null +++ b/PyCrypCli/models/service/public_service.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from ..model import Model + +if TYPE_CHECKING: + from ...client import Client + + +class PublicService(Model): + uuid: str + device_uuid: str = Field(alias="device") + name: str + running_port: int | None + + @staticmethod + def get_public_service(client: Client, device_uuid: str, service_uuid: str) -> PublicService: + return PublicService.parse( + client, client.ms("service", ["public_info"], device_uuid=device_uuid, service_uuid=service_uuid) + ) + + def update(self) -> PublicService: + return self._update(PublicService.get_public_service(self._client, self.device_uuid, self.uuid)) diff --git a/PyCrypCli/models/service/service.py b/PyCrypCli/models/service/service.py new file mode 100644 index 0000000..d200ec8 --- /dev/null +++ b/PyCrypCli/models/service/service.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import Any, TypeVar, TYPE_CHECKING + +from pydantic import Field + +from .public_service import PublicService +from ...exceptions import ServiceNotFoundError + +if TYPE_CHECKING: + from ...client import Client + +ServiceType = TypeVar("ServiceType", bound="Service") + + +class Service(PublicService): + owner_uuid: str = Field(alias="owner") + running: bool + part_owner_uuid: str | None = Field(alias="part_owner") + speed: float + + @staticmethod + def get_services(client: Client, device_uuid: str) -> list[Service]: + return [ + Service.parse(client, service) + for service in client.ms("service", ["list"], device_uuid=device_uuid)["services"] + ] + + @staticmethod + def get_service(client: Client, device_uuid: str, service_uuid: str) -> Service: + return Service.parse( + client, client.ms("service", ["private_info"], device_uuid=device_uuid, service_uuid=service_uuid) + ) + + @staticmethod + def get_service_by_name(client: Client, device_uuid: str, name: str) -> Service: + for service in Service.get_services(client, device_uuid): + if service.name == name: + return service + raise ServiceNotFoundError + + @staticmethod + def list_part_owner(client: Client) -> list[Service]: + return [Service.parse(client, service) for service in client.ms("service", ["list_part_owner"])["services"]] + + def update(self) -> Service: + return self._update(Service.get_service(self._client, self.device_uuid, self.uuid)) + + def use(self, **data: Any) -> dict[Any, Any]: + return self._ms("service", ["use"], device_uuid=self.device_uuid, service_uuid=self.uuid, **data) + + def toggle(self) -> Service: + return self._update(self._ms("service", ["toggle"], device_uuid=self.device_uuid, service_uuid=self.uuid)) + + def delete(self) -> None: + self._ms("service", ["delete"], device_uuid=self.device_uuid, service_uuid=self.uuid) diff --git a/PyCrypCli/models/shop/__init__.py b/PyCrypCli/models/shop/__init__.py new file mode 100644 index 0000000..25f0a9a --- /dev/null +++ b/PyCrypCli/models/shop/__init__.py @@ -0,0 +1,5 @@ +from .shop_category import ShopCategory +from .shop_product import ShopProduct + + +__all__ = ["ShopCategory", "ShopProduct"] diff --git a/PyCrypCli/models/shop/shop_category.py b/PyCrypCli/models/shop/shop_category.py new file mode 100644 index 0000000..f796a05 --- /dev/null +++ b/PyCrypCli/models/shop/shop_category.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from pydantic import validator, Field + +from .shop_product import ShopProduct +from ..model import Model + +if TYPE_CHECKING: + from ...client import Client + + +class ShopCategory(Model): + name: str + index: int + items: list[ShopProduct] + subcategories: list[ShopCategory] = Field(alias="categories") + + @staticmethod + def shop_list(client: Client) -> list[ShopCategory]: + out = [ + ShopCategory.parse(client, {"name": k, **v}) + for k, v in client.ms("inventory", ["shop", "list"])["categories"].items() + ] + out.sort(key=lambda c: c.index) + return out + + @validator("items", pre=True) + def _parse_items(cls, value: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: + out = [v | {"name": k} for k, v in value.items()] + out.sort(key=lambda e: e["id"]) + return out + + @validator("subcategories", pre=True) + def _parse_subcategories(cls, value: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: + out = [v | {"name": k} for k, v in value.items()] + out.sort(key=lambda e: e["index"]) + return out diff --git a/PyCrypCli/models/shop/shop_product.py b/PyCrypCli/models/shop/shop_product.py new file mode 100644 index 0000000..308b7ee --- /dev/null +++ b/PyCrypCli/models/shop/shop_product.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..inventory_element import InventoryElement +from ..model import Model +from ..wallet import Wallet + +if TYPE_CHECKING: + from ...client import Client + + +class ShopProduct(Model): + id: int + name: str + price: int + related_ms: str + + @staticmethod + def shop_info(client: Client, product: str) -> ShopProduct: + return ShopProduct.parse(client, client.ms("inventory", ["shop", "info"], product=product)) + + @staticmethod + def bulk_buy(client: Client, products: dict[ShopProduct, int], wallet: Wallet) -> list[InventoryElement]: + return [ + InventoryElement.parse(client, element) + for element in client.ms( + "inventory", + ["shop", "buy"], + products={k.name: v for k, v in products.items()}, + wallet_uuid=wallet.uuid, + key=wallet.key, + )["bought_products"] + ] + + def buy(self, wallet: Wallet) -> InventoryElement: + return ShopProduct.bulk_buy(self._client, {self: 1}, wallet)[0] diff --git a/PyCrypCli/models/wallet/__init__.py b/PyCrypCli/models/wallet/__init__.py new file mode 100644 index 0000000..5456867 --- /dev/null +++ b/PyCrypCli/models/wallet/__init__.py @@ -0,0 +1,6 @@ +from .public_wallet import PublicWallet +from .transaction import Transaction +from .wallet import Wallet + + +__all__ = ["PublicWallet", "Transaction", "Wallet"] diff --git a/PyCrypCli/models/wallet/public_wallet.py b/PyCrypCli/models/wallet/public_wallet.py new file mode 100644 index 0000000..4b72158 --- /dev/null +++ b/PyCrypCli/models/wallet/public_wallet.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from ..model import Model + +if TYPE_CHECKING: + from ...client import Client + + +class PublicWallet(Model): + uuid: str = Field(alias="source_uuid") + + @staticmethod + def get_public_wallet(client: Client, uuid: str) -> PublicWallet: + return PublicWallet.parse(client, {"source_uuid": uuid}) + + @staticmethod + def list_wallets(client: Client) -> list[PublicWallet]: + return [PublicWallet.get_public_wallet(client, uuid) for uuid in client.ms("currency", ["list"])["wallets"]] + + def reset_wallet(self) -> None: + self._ms("currency", ["reset"], source_uuid=self.uuid) diff --git a/PyCrypCli/models/wallet/transaction.py b/PyCrypCli/models/wallet/transaction.py new file mode 100644 index 0000000..0efe0d7 --- /dev/null +++ b/PyCrypCli/models/wallet/transaction.py @@ -0,0 +1,19 @@ +from datetime import datetime + +from pydantic import Field, validator + +from ..model import Model +from ...util import utc_to_local + + +class Transaction(Model): + timestamp: datetime = Field(alias="time_stamp") + source_uuid: str + destination_uuid: str + amount: int = Field(alias="send_amount") + usage: str + origin: str + + @validator("timestamp") + def _convert_timestamp(cls, value: datetime) -> datetime: + return utc_to_local(value) diff --git a/PyCrypCli/models/wallet/wallet.py b/PyCrypCli/models/wallet/wallet.py new file mode 100644 index 0000000..a2b1581 --- /dev/null +++ b/PyCrypCli/models/wallet/wallet.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from .public_wallet import PublicWallet +from .transaction import Transaction +from ..service import Miner + +if TYPE_CHECKING: + from ...client import Client + + +class Wallet(PublicWallet): + key: str + owner_uuid: str = Field(alias="user_uuid") + amount: int + transaction_count: int = Field(alias="transactions") + + @staticmethod + def create_wallet(client: Client) -> Wallet: + return Wallet.parse(client, client.ms("currency", ["create"])) + + @staticmethod + def get_wallet(client: Client, uuid: str, key: str) -> Wallet: + return Wallet.parse(client, client.ms("currency", ["get"], source_uuid=uuid, key=key)) + + def update(self) -> Wallet: + return self._update(Wallet.get_wallet(self._client, self.uuid, self.key)) + + def get_transactions(self, count: int, offset: int) -> list[Transaction]: + return [ + Transaction.parse(self._client, t) + for t in self._ms( + "currency", ["transactions"], source_uuid=self.uuid, key=self.key, count=count, offset=offset + )["transactions"] + ] + + def get_miners(self) -> list[Miner]: + return Miner.get_miners(self._client, self.uuid) + + def get_mining_rate(self) -> float: + return sum(miner.speed for miner in self.get_miners() if miner.running) + + def send(self, destination: PublicWallet, amount: int, usage: str) -> Wallet: + self._ms( + "currency", + ["send"], + source_uuid=self.uuid, + key=self.key, + send_amount=amount, + destination_uuid=destination.uuid, + usage=usage, + ) + return self.update() + + def delete(self) -> None: + self._ms("currency", ["delete"], source_uuid=self.uuid, key=self.key) diff --git a/PyCrypCli/pycrypcli.py b/PyCrypCli/pycrypcli.py index 1c38b65..421c413 100644 --- a/PyCrypCli/pycrypcli.py +++ b/PyCrypCli/pycrypcli.py @@ -1,18 +1,18 @@ -import os import sys from os import getenv -from typing import List, Optional +from pathlib import Path +from typing import NoReturn import requests import sentry_sdk -from PyCrypCli.commands import make_commands, Command -from PyCrypCli.context import Context, LoginContext, RootContext +from .commands import make_commands, Command +from .context import Context, LoginContext, RootContext try: import readline except ImportError: - import pyreadline as readline + import pyreadline as readline # type: ignore if not getenv("DEBUG"): response = requests.get("https://sentrydsn.defelo.de/pycrypcli") @@ -21,10 +21,10 @@ class Frontend: - def __init__(self, server: str, config_file: List[str]): - self.config_file: List[str] = config_file + def __init__(self, server: str, config_file: Path): + self.config_file: Path = config_file - self.history: List[str] = [] + self.history: list[str] = [] readline.parse_and_bind("tab: complete") readline.set_completer(self.completer) @@ -33,8 +33,8 @@ def __init__(self, server: str, config_file: List[str]): self.root_context: RootContext = RootContext(server, config_file, make_commands()) self.root_context.open(LoginContext(self.root_context)) - def complete_command(self, text: str) -> List[str]: - override_completions: Optional[List[str]] = self.root_context.get_override_completions() + def complete_command(self, text: str) -> list[str]: + override_completions: list[str] | None = self.root_context.get_override_completions() if override_completions is not None: return override_completions @@ -42,16 +42,16 @@ def complete_command(self, text: str) -> List[str]: if not args: return list(self.root_context.get_commands()) - comp: Optional[Command] = self.root_context.get_commands().get(cmd, None) + comp: Command | None = self.root_context.get_commands().get(cmd, None) if comp is None: return [] return comp.handle_completer(self.get_context(), args) or [] - def completer(self, text: str, state: int) -> Optional[str]: + def completer(self, text: str, state: int) -> str | None: readline.set_completer_delims(" ") - options: List[str] = self.complete_command(readline.get_line_buffer()) - options: List[str] = [o + " " if o[-1:] != "\0" else o[:-1] for o in sorted(options) if o.startswith(text)] + options: list[str] = self.complete_command(readline.get_line_buffer()) + options = [o + " " if o[-1:] != "\0" else o[:-1] for o in sorted(options) if o.startswith(text)] if state < len(options): return options[state] @@ -60,7 +60,7 @@ def completer(self, text: str, state: int) -> Optional[str]: def get_context(self) -> Context: return self.root_context.get_context() - def mainloop(self): + def mainloop(self) -> NoReturn: while True: self.get_context().loop_tick() context: Context = self.get_context() @@ -88,7 +88,7 @@ def mainloop(self): print("Type `help` for a list of commands.") -def main(): +def main() -> NoReturn: print( "\033[32m\033[1m" r""" @@ -99,20 +99,20 @@ def main(): \____/_/ \__, / .___/\__/_/\___/ /____/_/ """ - "\033[0m", + "\033[0m" ) print("Python Cryptic Game Client (https://github.com/Defelo/PyCrypCli)") print("You can always type `help` for a list of available commands.") - server: str = "wss://ws.cryptic-game.net/" + server = "wss://ws.cryptic-game.net/" if len(sys.argv) > 1: - server: str = sys.argv[1] + server = sys.argv[1] if server.lower() == "test": - server: str = "wss://ws.test.cryptic-game.net/" + server = "wss://ws.test.cryptic-game.net/" elif not server.startswith("wss://") and not server.startswith("ws://"): - server: str = "ws://" + server + server = "ws://" + server - frontend: Frontend = Frontend(server, [os.path.expanduser("~"), ".config", "PyCrypCli", "config.json"]) + frontend: Frontend = Frontend(server, Path("~/.config/PyCrypCli/config.json").expanduser()) frontend.mainloop() diff --git a/PyCrypCli/timer.py b/PyCrypCli/timer.py index de95c42..3696739 100644 --- a/PyCrypCli/timer.py +++ b/PyCrypCli/timer.py @@ -1,18 +1,18 @@ import time from threading import Thread -from typing import Callable +from typing import Callable, Any class Timer(Thread): - def __init__(self, interval: float, func: Callable): + def __init__(self, interval: float, func: Callable[[], Any]): super().__init__(daemon=True) self.interval: float = interval - self.func: Callable = func + self.func: Callable[[], Any] = func self.running: bool = False - def run(self): - self.running: bool = True + def run(self) -> None: + self.running = True while self.running: self.func() @@ -20,7 +20,7 @@ def run(self): sleep_until: float = t + self.interval while t < sleep_until and self.running: time.sleep(min(0.1, sleep_until - t)) - t: float = time.time() + t = time.time() - def stop(self): - self.running: bool = False + def stop(self) -> None: + self.running = False diff --git a/PyCrypCli/util.py b/PyCrypCli/util.py index 7611a01..8601f86 100644 --- a/PyCrypCli/util.py +++ b/PyCrypCli/util.py @@ -1,30 +1,28 @@ import re -from datetime import datetime -from typing import Optional, Tuple, List - -from dateutil.tz import tz +from datetime import datetime, timezone +from typing import Any, Sequence def is_uuid(x: str) -> bool: return bool(re.match(r"^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$", x)) -def extract_wallet(content: str) -> Optional[Tuple[str, str]]: +def extract_wallet(content: str) -> tuple[str, str] | None: if re.match(r"^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12} [0-9a-f]{10}$", content): uuid, key = content.split() return uuid, key return None -def convert_timestamp(timestamp: datetime) -> datetime: - return timestamp.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).replace(tzinfo=None) +def utc_to_local(timestamp: datetime) -> datetime: + return timestamp.replace(tzinfo=timezone.utc).astimezone().replace(tzinfo=None) -def strip_float(num: float, precision): +def strip_float(num: float, precision: int) -> str: return f"{num:.{precision}f}".rstrip("0").rstrip(".") -def print_tree(items: List[Tuple[str, Optional[list]]], indent: Optional[List[bool]] = None): +def print_tree(items: Sequence[tuple[str, Sequence[Any] | None]], indent: list[bool] | None = None) -> None: if not indent: indent = [] for i, (item, children) in enumerate(items): diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..e95c0b4 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +strict = True +ignore_missing_imports = True +plugins=pydantic.mypy diff --git a/pycrypcli.py b/pycrypcli.py deleted file mode 100755 index 3e9076c..0000000 --- a/pycrypcli.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 - -from PyCrypCli.pycrypcli import main - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 55ec8d7..19ef237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,4 @@ [tool.black] +target-version = ["py310"] line-length = 120 +skip-magic-trailing-comma = true diff --git a/requirements.txt b/requirements.txt index c3a4222..9e790b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ pyreadline~=2.1 pypresence~=4.2 python-dateutil~=2.8 sentry-sdk~=1.5 -requests~=2.27 \ No newline at end of file +requests~=2.27 +pydantic~=1.9