Compare commits

..

No commits in common. "c00aa232fc68ebcc3745ed6a268ab0ae95a78d6e" and "d79a01cfb14494cf8c90d9c50f7918e19205544f" have entirely different histories.

16 changed files with 69 additions and 86 deletions

1
dockge_cli/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.temp/*

View file

@ -4,18 +4,15 @@ from getpass import getpass
from ...models import Credentials
from ...service import storage
from ...service.communicate import DockgeConnection
from ..utils import stack_formatter, status_formatter, generic_formatter, credential_parser_factory
from ..utils import stack_formatter, status_formatter, generic_formatter, get_credential_parser
class FunctionBindings():
class ExecutionCommands():
"""
Helper class that provides all the static methods in an organized way
This is an abstraction layer of the CLI, as those functions only do little preprocessing before calling the actural DockgeConnection
"""
@staticmethod
def __setup():
"""
Creates a connection and logs into Dockge
"""
con = DockgeConnection()
con.connect_and_login()
return con
@ -39,9 +36,8 @@ class FunctionBindings():
"""
login command binding
"""
print(f"WARNING! These credentials will be saved unencrypted in {storage._file.absolute()}")
if len(extra_args) > 0:
credentials = credential_parser_factory().parse_args(extra_args, namespace=Credentials)
credentials = get_credential_parser().parse_args(extra_args, namespace=Credentials)
storage.put("username", credentials.username, encoded=True)
storage.put("password", credentials.password, encoded=True)
return
@ -68,7 +64,7 @@ class FunctionBindings():
"""
list command binding
"""
con = FunctionBindings.__setup()
con = ExecutionCommands.__setup()
stack_formatter(con.list_stacks())
con.disconnect()
@ -77,7 +73,9 @@ class FunctionBindings():
"""
status command binding
"""
con = FunctionBindings.__setup()
if extra_args is None:
raise ValueError
con = ExecutionCommands.__setup()
status_formatter(con.list_stack(extra_args[0]))
con.disconnect()
@ -86,7 +84,9 @@ class FunctionBindings():
"""
restart command binding
"""
con = FunctionBindings.__setup()
if extra_args is None:
raise ValueError
con = ExecutionCommands.__setup()
generic_formatter(con.restart(extra_args[0]))
con.disconnect()
@ -95,7 +95,9 @@ class FunctionBindings():
"""
update command binding
"""
con = FunctionBindings.__setup()
if extra_args is None:
raise ValueError
con = ExecutionCommands.__setup()
generic_formatter(con.update(extra_args[0]))
con.disconnect()
@ -104,7 +106,9 @@ class FunctionBindings():
"""
stop command binding
"""
con = FunctionBindings.__setup()
if extra_args is None:
raise ValueError
con = ExecutionCommands.__setup()
generic_formatter(con.stop(extra_args[0]))
con.disconnect()
@ -113,7 +117,9 @@ class FunctionBindings():
"""
start command binding
"""
con = FunctionBindings.__setup()
if extra_args is None:
raise ValueError
con = ExecutionCommands.__setup()
generic_formatter(con.start(extra_args[0]))
con.disconnect()
@ -122,7 +128,9 @@ class FunctionBindings():
"""
down command binding
"""
con = FunctionBindings.__setup()
if extra_args is None:
raise ValueError
con = ExecutionCommands.__setup()
generic_formatter(con.down(extra_args[0]))
con.disconnect()

View file

@ -1,6 +1,6 @@
from typing import List
from ...models import Command
from .functions import FunctionBindings
from .bindings import ExecutionCommands
mapping: List[Command] = [
Command(
@ -8,83 +8,83 @@ mapping: List[Command] = [
description="Sets and gets the URI of the dockge instance. Remove any unnecessary subdomains/protocols from the URI",
args=1,
optional=True,
func=FunctionBindings.host
bind=ExecutionCommands.host
),
Command(
cmd="login",
description="Logs into a given dockge account, either with an interactive dialogue or by passing --user and --password",
args=2,
optional=True,
func=FunctionBindings.login
bind=ExecutionCommands.login
),
Command(
cmd="logout",
description="Removes the credentials from the local storage.",
args=0,
optional=False,
func=FunctionBindings.logout
bind=ExecutionCommands.logout
),
Command(
cmd="list",
description="Lists all available stacks with their status",
args=0,
optional=False,
func=FunctionBindings.list
bind=ExecutionCommands.list
),
Command(
cmd="status",
description="Returns the status of one stack",
args=1,
optional=False,
func=FunctionBindings.status
bind=ExecutionCommands.status
),
Command(
cmd="restart",
description="Restarts a given stack",
args=1,
optional=False,
func=FunctionBindings.restart
bind=ExecutionCommands.restart
),
Command(
cmd="start",
description="Starts a given stack",
args=1,
optional=False,
func=FunctionBindings.start
bind=ExecutionCommands.start
),
Command(
cmd="stop",
description="Stops a given stack",
args=1,
optional=False,
func=FunctionBindings.stop
bind=ExecutionCommands.stop
),
Command(
cmd="down",
description="Stop & Downs a given stack",
args=1,
optional=False,
func=FunctionBindings.down
bind=ExecutionCommands.down
),
Command(
cmd="update",
description="Updates a stack",
args=1,
optional=False,
func=FunctionBindings.update
bind=ExecutionCommands.update
),
Command(
cmd="exit",
description="Exits the CLI - this will reset all settings, including credentials and host",
args=0,
optional=False,
func=FunctionBindings.exit
bind=ExecutionCommands.exit
),
Command(
cmd="help",
description="Displays helping hints for commands",
args=1,
optional=True,
func=FunctionBindings.help
bind=ExecutionCommands.help
)
]

View file

@ -0,0 +1,7 @@
from ...models import Command
from .descriptors import mapping
commands: dict[str, Command] = {}
for descriptor in mapping:
commands.update({ descriptor.cmd: descriptor })

View file

@ -1,4 +0,0 @@
from ...models import Command
from .mappings import mapping
commands: dict[str, Command] = { c.cmd: c for c in mapping }

View file

@ -3,7 +3,7 @@ import sys
from .. import __version__
from ..models import Arguments
from .commands import commands
from .commandprovider.factory import commands
def parse_arguments():
"""

View file

@ -1,4 +1,4 @@
from .commands import commands
from .commandprovider.factory import commands
def display_help(extra_args):
"""
@ -32,4 +32,4 @@ def run(command, args):
display_help(args)
return
c.func(args)
c.bind(args)

View file

@ -2,7 +2,7 @@ import argparse
from tabulate import tabulate
from ..models import StackStatus
def credential_parser_factory():
def get_credential_parser():
"""
Creates a new parser for login credentials
"""
@ -23,7 +23,7 @@ def stack_formatter(stacks):
table, headers = [], ["Stackname", "Status"]
for key, val in stacks["stackList"].items():
table.append([key, StackStatus(val['status']).name.lower()])
table.append([key, StackStatus(val['status']).name])
print(tabulate(table, headers=headers, tablefmt="github"), "\n")

View file

@ -1,3 +1,3 @@
from .codes import StackStatus
from .command import Command
from .commands import Command
from .parser import Arguments, Credentials

View file

@ -2,8 +2,9 @@ from enum import Enum
class StackStatus(Enum):
"""
mapping for plaintext vs statuscode
mapping codes for status vs text
"""
INACTIVE = 1
RUNNING = 3
EXITED = 4
# pylint: disable=invalid-name
inactive = 1
running = 3
exited = 4

View file

@ -6,7 +6,7 @@ class Command(BaseModel):
Basic command structure for the CLI to automatically generate valid commands
"""
cmd: str
func: Callable
bind: Callable
args: int
optional: bool
description: str

View file

@ -1 +0,0 @@
.storage/*

View file

@ -32,7 +32,7 @@ class DockgeConnection:
def _init_events(self):
@self._sio.event
def connect():
self.login()
self.connect()
print("Connected!")
@self._sio.event
@ -70,11 +70,10 @@ class DockgeConnection:
"""
Connect to the websocket
"""
# Dockge uses Socket.io for the websockets, so this URI and params are always the same
self._sio.connect(f"https://{self._host}/socket.io/", transports=['websocket'])
self.login()
self.connect()
def login(self):
def connect(self):
"""
Log into dockge using basicauth
Retries 5 times when timeouts occur
@ -85,11 +84,6 @@ class DockgeConnection:
data = None
retry, count = True, 0
if not storage.exists("username"):
raise ValueError("Missing username")
if not storage.exists("password"):
raise ValueError("Missing password")
while retry and count < 5:
try:
data = self._sio.call(

View file

@ -3,44 +3,28 @@ import pathlib
import base64
import yaml
_storagepath = pathlib.Path(__file__).parent / ".storage"
_storagepath.mkdir(exist_ok=True, parents=True)
_storagepath = pathlib.Path(__file__).parents[1] / ".temp"
_file = _storagepath / "storage.yaml"
def create_file_when_missing():
_storagepath.mkdir(exist_ok=True, parents=True)
def fileexists():
"""
Checks if storage file does exist, creates it when necessary
"""
if _file.exists():
return
if not _file.exists():
with open(_file, 'a', encoding="utf-8"):
os.utime(_file, None)
def exists(key: str) -> bool:
"""
Checks if a given key exists in the storage file
"""
if not _file.exists():
return False
with open(_file, "r", encoding="utf-8") as file:
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader)
return key in content
def put(key: str, value: str, encoded=False):
"""
Puts a given value with a given key into the storage file
Encodes the data as base64 when encoded is set to true
"""
if not _file.exists():
create_file_when_missing()
with open(_file, "r", encoding="utf-8") as file:
fileexists()
with open(_file, "r+", encoding="utf-8") as file:
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
content.update({ key: str(base64.b64encode(value.encode()), "utf-8") if encoded else value })
with open(_file, "w+", encoding="utf-8") as file:
yaml.dump(content, file, Dumper=yaml.SafeDumper)
@ -48,13 +32,10 @@ def remove(key: str):
"""
Removed a given key from the storage file
"""
if not _file.exists():
create_file_when_missing()
fileexists()
with open(_file, "r", encoding="utf-8") as file:
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
content.pop(key, None)
with open(_file, "w+", encoding="utf-8") as file:
yaml.dump(content, file, Dumper=yaml.SafeDumper)
@ -66,11 +47,9 @@ def get(key: str, encoded=False):
value: str | None = None
if not _file.exists():
return None
with open(_file, "r", encoding="utf-8") as file:
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader)
value = content.get(key, None)
if value is None:
return None
return base64.b64decode(value.encode()).decode() if encoded else value
@ -79,6 +58,4 @@ def clear():
"""
Deletes the storage file
"""
if not _file.exists():
return
_file.unlink()

View file

@ -1,6 +1,6 @@
[project]
name = "dockge_cli"
version = "0.1.0-c.3"
version = "0.1.0-c.2"
dependencies = [
"pyyaml~=6.0.1",
"pydantic~=2.8.0",