Added mypy and pydantic

This commit is contained in:
Felix Bargfeldt 2022-03-08 17:56:13 +01:00
parent afa09a2698
commit 986c3dee45
No known key found for this signature in database
GPG key ID: 87CB84A4087C3603
92 changed files with 1986 additions and 1755 deletions

View file

@ -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

View file

@ -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:

View file

@ -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"]

View file

@ -2,5 +2,4 @@
from PyCrypCli.pycrypcli import main
if __name__ == "__main__":
main()
main()

View file

@ -2,24 +2,27 @@ import json
import re
import ssl
import time
from typing import List, Optional, Type
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 .models import HardwareConfig, StatusResponse, InfoResponse, TokenResponse
from PyCrypCli.timer import Timer
@ -30,184 +33,213 @@ 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
def request(self, data: dict, no_response: bool = False) -> dict:
self.logged_in = False
def _send(self, obj: dict[str, Any]) -> None:
if not self.websocket:
raise ClientNotReadyError
data = json.dumps(obj)
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
data = self.websocket.recv()
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], **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)
raise InvalidServerResponseError(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:
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)
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:
self.close()
raise InvalidServerResponseException(response)
self.logged_in: bool = True
self.timer.start()
return response["token"]
raise UsernameAlreadyExistsError()
raise InvalidServerResponseError(response)
def login(self, username: str, password: str) -> str:
try:
token_response = TokenResponse.parse(self, response)
except ValidationError:
self.close()
raise
self.logged_in = True
cast(Timer, self.timer).start()
return token_response
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:
self.close()
raise InvalidServerResponseException(response)
self.logged_in: bool = True
self.timer.start()
return response["token"]
raise InvalidLoginError()
raise InvalidServerResponseError(response)
def session(self, token: str):
try:
token_response = TokenResponse.parse(self, response)
except ValidationError:
self.close()
raise
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
def change_password(self, old_password: str, new_password: str) -> str:
self.logged_in = True
cast(Timer, self.timer).start()
return token_response
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"]))

View file

@ -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"]

View file

@ -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
"""

View file

@ -1,8 +1,10 @@
from importlib import import_module
from typing import Callable, List, Dict, Type, Optional, Tuple
from __future__ import annotations
from PyCrypCli.context import Context, COMMAND_FUNCTION, COMPLETER_FUNCTION
from PyCrypCli.exceptions import CommandRegistrationException, NoDocStringException
from importlib import import_module
from typing import Callable, Type
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 @@ class Command:
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 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

View file

@ -1,28 +1,28 @@
from typing import List, Dict, Optional
from typing import Any, cast
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 .command import command, CommandError
from .help import print_help
from ..context import MainContext, DeviceContext
from ..exceptions import (
AlreadyOwnADeviceError,
DeviceNotFoundError,
IncompatibleCPUSocketError,
NotEnoughRAMSlotsError,
IncompatibleRAMTypesError,
IncompatibleDriverInterfaceError,
)
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 +30,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 +47,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 +55,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 +65,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 +75,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 +83,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 +91,42 @@ def handle_device_build(context: MainContext, args: List[str]):
if len(args) < 5:
raise CommandError("usage: device build <mainboard> <cpu> <gpu> <ram> [<ram>...] <disk> [<disk>...]")
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 +139,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 +166,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 +182,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 +200,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 +212,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 +226,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
"""
@ -248,31 +248,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 +289,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 +297,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()

View file

@ -1,43 +1,39 @@
from typing import List, Optional, Tuple
from typing import Any, cast
from PyCrypCli.exceptions import (
FileAlreadyExistsException,
InvalidWalletFile,
UnknownSourceOrDestinationException,
PermissionDeniedException,
FileNotChangeableException,
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 <dirname>")
*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 <filepath>")
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>")
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.")
@ -169,7 +167,7 @@ def handle_rm(context: DeviceContext, args: List[str]):
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 this 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 <source> <destination>")
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 <source> <destination>")
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 []

View file

@ -1,16 +1,14 @@
from typing import List, Optional, Dict, Tuple
from PyCrypCli.commands import command, Command, CommandError
from PyCrypCli.context import LoginContext, MainContext, DeviceContext, Context
from .command import command, Command, CommandError
from ..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 []

View file

@ -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.")

View file

@ -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.")

View file

@ -1,42 +1,42 @@
import re
import time
from typing import List
from typing import Any
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 .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 <filename> <receiver> <amount> [usage]\n" " or: pay <uuid> <key> <receiver> <amount> [usage]",
"usage: pay <filename> <receiver> <amount> [usage]\n" " or: pay <uuid> <key> <receiver> <amount> [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 []

View file

@ -1,39 +1,39 @@
from typing import List
from typing import Any
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 .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]} <network> [<device>]")
@ -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 []

View file

@ -1,36 +1,36 @@
import os
import time
from typing import List, Tuple
from typing import Any, cast
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 .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 <wallet>")
@ -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 <target-device> <target-service> [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 []

View file

@ -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:

View file

@ -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:

View file

@ -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}")

View file

@ -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",
]

View file

@ -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 @@ class Context:
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 @@ class Context:
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]]

View file

@ -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 @@ class DeviceContext(MainContext):
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 @@ class DeviceContext(MainContext):
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 @@ class DeviceContext(MainContext):
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 @@ class DeviceContext(MainContext):
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])

View file

@ -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 @@ class LoginContext(Context):
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):
def load_session(self) -> None:
from PyCrypCli.context.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",

View file

@ -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 @@ class MainContext(LoginContext):
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 @@ class MainContext(LoginContext):
)
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)}
)

View file

@ -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.context_stack: List[Context] = []
self.host: str = match.group(1).split("/")[0]
self.commands: Dict[Type[Context], Dict[str, "Command"]] = commands
self.config_file: Path = config_file
self.context_stack: list[Context] = []
self.commands: dict[Type[Context], dict[str, Command]] = commands
try:
self.presence: Presence = Presence(client_id="596676243144048640")
@ -34,35 +36,29 @@ class RootContext:
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 read_config_file(self) -> Config:
if not self.config_file.exists():
self.write_config_file(Config.get_default_config())
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()
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())

View file

@ -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"

View file

@ -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",
]

View file

@ -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"]
]

View file

@ -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")

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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"]

View file

@ -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)

View file

@ -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)

View file

@ -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")

View file

@ -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"])

View file

@ -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"]

View file

@ -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"]

View file

@ -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()

View file

@ -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"]]

View file

@ -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))

View file

@ -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)

View file

@ -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"]

View file

@ -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

View file

@ -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]

View file

@ -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")

View file

@ -1,5 +0,0 @@
from PyCrypCli.game_objects.wallet.public_wallet import PublicWallet
from PyCrypCli.game_objects.wallet.wallet import Wallet
__all__ = ["Wallet", "PublicWallet"]

View file

@ -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)

View file

@ -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)

View file

@ -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",
]

View file

@ -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={})

129
PyCrypCli/models/device.py Normal file
View file

@ -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"]
]

View file

@ -0,0 +1,8 @@
from .model import Model
class DeviceHardware(Model):
uuid: str
device_uuid: str
hardware_element: str
hardware_type: str

69
PyCrypCli/models/file.py Normal file
View file

@ -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)

View file

@ -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",
]

View file

@ -0,0 +1,8 @@
from typing import Literal
from ..model import Model
class Case(Model):
id: int
size: Literal["small", "middle", "big"]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -0,0 +1,8 @@
from pydantic import Field
from ..model import Model
class PowerPack(Model):
id: int
total_power: int = Field(alias="totalPower")

View file

@ -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

View file

@ -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

View file

@ -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)

30
PyCrypCli/models/model.py Normal file
View file

@ -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

View file

@ -0,0 +1,5 @@
from .network import Network
from .network_invitation import NetworkInvitation
from .network_membership import NetworkMembership
__all__ = ["Network", "NetworkInvitation", "NetworkMembership"]

View file

@ -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)

View file

@ -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)

View file

@ -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")

View file

@ -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)

View file

@ -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

View file

@ -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"]

View file

@ -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"]

View file

@ -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"], 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()

View file

@ -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"]]

View file

@ -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))

View file

@ -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)

View file

@ -0,0 +1,5 @@
from .shop_category import ShopCategory
from .shop_product import ShopProduct
__all__ = ["ShopCategory", "ShopProduct"]

View file

@ -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

View file

@ -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]

View file

@ -0,0 +1,6 @@
from .public_wallet import PublicWallet
from .transaction import Transaction
from .wallet import Wallet
__all__ = ["PublicWallet", "Transaction", "Wallet"]

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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 @@ if not getenv("DEBUG"):
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 @@ class Frontend:
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 @@ class Frontend:
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 @@ class Frontend:
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 @@ class Frontend:
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()

View file

@ -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 @@ class Timer(Thread):
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

View file

@ -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):

4
mypy.ini Normal file
View file

@ -0,0 +1,4 @@
[mypy]
strict = True
ignore_missing_imports = True
plugins=pydantic.mypy

View file

@ -1,2 +1,4 @@
[tool.black]
target-version = ["py310"]
line-length = 120
skip-magic-trailing-comma = true

View file

@ -3,4 +3,5 @@ pyreadline~=2.1
pypresence~=4.2
python-dateutil~=2.8
sentry-sdk~=1.5
requests~=2.27
requests~=2.27
pydantic~=1.9