diff --git a/.forgejo/workflows/build-release.yaml b/.forgejo/workflows/build-release.yaml index 48e5e92..1170c10 100644 --- a/.forgejo/workflows/build-release.yaml +++ b/.forgejo/workflows/build-release.yaml @@ -6,7 +6,7 @@ on: - '[0-9]+\.[0-9]+\.[0-9]' jobs: - lint-and-typing: + backend-pylint: runs-on: docker container: nikolaik/python-nodejs:python3.11-nodejs21 steps: @@ -14,19 +14,16 @@ jobs: uses: https://code.forgejo.org/actions/checkout@v3 - name: Install packages run: | - pip install -e .[lint,typing] -q --disable-pip-version-check -q + pip install -e . -q python -m pip list --format=columns --disable-pip-version-check + pip install pylint~=2.17.7 --disable-pip-version-check -q - name: Run pylint run: | pylint --version pylint **/*.py --exit-zero --rc-file pyproject.toml - - name: Run mypy - run: | - mypy --version - mypy . build-artifacts: - needs: ["lint-and-typing"] + needs: ["backend-pylint"] runs-on: docker container: nikolaik/python-nodejs:python3.11-nodejs21 steps: @@ -56,37 +53,3 @@ jobs: run: pip install twine - name: Upload package to registry run: python -m twine upload --repository-url ${{ secrets.REPOSITORY_URL }} -u ${{ secrets.TWINE_DEPLOY_USER }} -p ${{ secrets.TWINE_DEPLOY_PASSWORD }} dist/* - - build-and-push-container: - needs: [ "publish-artifacts" ] - runs-on: dind - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Log into Docker Package Registry - uses: docker/login-action@v3 - with: - registry: forgejo.neshweb.net - username: ${{ secrets.FORGEJO_USERNAME }} - password: ${{ secrets.FORGEJO_TOKEN }} - - name: Build and push to Docker Package Registry - uses: docker/build-push-action@v5 - with: - build-args: | - PACKAGE_VERSION=${{ github.ref_name }} - push: true - tags: forgejo.neshweb.net/firq/dockge-cli:${{ github.ref_name }} - - release: - needs: [ build-and-push-container, publish-artifacts ] - if: success() - runs-on: docker - steps: - - name: Release New Version - uses: actions/forgejo-release@v1 - with: - direction: upload - url: https://forgejo.neshweb.net - release-dir: release - token: ${{ secrets.FORGEJO_TOKEN }} - tag: ${{ github.ref_name }} \ No newline at end of file diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml deleted file mode 100644 index 55de02a..0000000 --- a/.forgejo/workflows/check.yaml +++ /dev/null @@ -1,34 +0,0 @@ -on: - push: - branches: "**" - -jobs: - pylint: - runs-on: docker - container: nikolaik/python-nodejs:python3.11-nodejs21 - steps: - - name: Checkout source code - uses: https://code.forgejo.org/actions/checkout@v3 - - name: Install packages - run: | - pip install -e .[lint] -q --disable-pip-version-check -q - python -m pip list --format=columns --disable-pip-version-check - - name: Run pylint - run: | - pylint --version - pylint **/*.py --exit-zero --rc-file pyproject.toml - - mypy: - runs-on: docker - container: nikolaik/python-nodejs:python3.11-nodejs21 - steps: - - name: Checkout source code - uses: https://code.forgejo.org/actions/checkout@v3 - - name: Install packages - run: | - pip install -e .[typing] -q --disable-pip-version-check -q - python -m pip list --format=columns --disable-pip-version-check - - name: Run mypy - run: | - mypy --version - mypy . diff --git a/.forgejo/workflows/lint.yaml b/.forgejo/workflows/lint.yaml new file mode 100644 index 0000000..a88449c --- /dev/null +++ b/.forgejo/workflows/lint.yaml @@ -0,0 +1,20 @@ +on: + push: + branches: "**" + +jobs: + backend-pylint: + runs-on: docker + container: nikolaik/python-nodejs:python3.11-nodejs21 + steps: + - name: Checkout source code + uses: https://code.forgejo.org/actions/checkout@v3 + - name: Install packages + run: | + pip install -e . -q + python -m pip list --format=columns --disable-pip-version-check + pip install pylint~=2.17.7 --disable-pip-version-check -q + - name: Run pylint + run: | + pylint --version + pylint **/*.py --exit-zero --rc-file pyproject.toml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 88e754d..0000000 --- a/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM forgejo.neshweb.net/ci-docker-images/python-neshweb:3.11 - -ARG PACKAGE_VERSION=0.1.0 -RUN pip install dockge-cli==${PACKAGE_VERSION} diff --git a/README.md b/README.md index 2f946e3..4c51468 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,3 @@ # dockge-cli A simple CLI application written in Python for communicating with Dockge using websockets - -## Background - -Dockge (spoken dock-ge or dockage) is a tool to manage docker-compose stacks from a web ui. It is developed by louislam, who also develops UptimeKuma. - -Dockge itself doesn't offer any kind of API or programmatic access, as it is just intended for managing stacks via UI. - -My current deployment solution for firq.dev and fgo-ta.com is based on Dockge, and I was over it always having to reload the stack whenever I pushed an update. Instead, I wanted to have this as a separate CI step, automatically redeploying a givens stack. - -As Dockge is using a websocket-based system under the hood, it was easy to take a look at how communication occurs. In general, communication is achieved by leveraging socket.io for the data. Since Python already offers a solution for socket.io, it is just a matter of emulating the calls the webui sends and receives. - -In the end, this is the current result that works pretty well for my understanding. I am still trying to improve upon some issues (login times out, stability, features), but in general this works as a fine solution for automatic stack updating. - -## Installation - -Install it from the custom package index using - -```shell -pip install --extra-index-url https://forgejo.neshweb.net/api/packages/Firq/pypi/simple/ dockge-cli -``` - -Alternativly, install it using this repository. When installing for development, make sure to install with the additional dependencies - -```shell -pip install -e .[lint,typing] -``` - -## Usage - -Call the CLI using `dockge-cli` or `dockge`. - -```shell -usage: dockge_cli [-h] [--version] {host,login,logout,list,status,restart,start,stop,down,update,exit,help} - -CLI interface for interacting with Dockge - -positional arguments: - {host,login,logout,list,status,restart,start,stop,down,update,exit,help} - -options: - -h, --help show this help message and exit - --version show program's version number and exit -``` - -Help for each individual command can be invoked by calling `dockge-cli help <command>` - -## The magic behind this - -Generally, this makes use of the underlying Websockets API that the Dockge frontend uses to communicate with the server. By analyzing the traffic and looking into the codebase, I was able to reverse most of the packets that are being sent. This allows me to then contruct, send and receive my own packets, making the whole thing work. - -There are some things that need to be taken into account for this: For one, dockge uses socket.io for the websocket communication. This meant I had to find the corresponding socket.io version to get the correct version of python-socketio. In addition, I had to find out how the authorization mechanism behind this works. - -After finishing up the first prototype, the workings are as follows: - -1. A websocket session is established using socket.io - this happens automatically -2. After the session is ready, the `login` command is sent together with a provided username and password -3. Once the CLI is authorized, the selected command is sent -4. The CLI waits for any response values and exits once the command has executed successfully - -To provide a smooth experience, both the credentials and the remote host URI are stored on disk. just like the `docker` cli, the credentials are not encrypted, meaning it is advised to either clear the credentials after use OR to use the `--username` and `--password` parameters. This is especially recommended for CI applications. - -## Known issues - -This CLI does not work when Mullvad is used, as Mullvad actively blocks port forwarding (which python-socketio uses) diff --git a/dockge_cli/.gitignore b/dockge_cli/.gitignore new file mode 100644 index 0000000..d36a0d4 --- /dev/null +++ b/dockge_cli/.gitignore @@ -0,0 +1 @@ +.temp/* \ No newline at end of file diff --git a/dockge_cli/__main__.py b/dockge_cli/__main__.py deleted file mode 100644 index 660e368..0000000 --- a/dockge_cli/__main__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .dockge_cli import cli -cli() diff --git a/dockge_cli/client/commandprovider/__init__.py b/dockge_cli/client/commandprovider/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dockge_cli/client/commands/functions.py b/dockge_cli/client/commandprovider/bindings.py similarity index 50% rename from dockge_cli/client/commands/functions.py rename to dockge_cli/client/commandprovider/bindings.py index 3ee695d..6e813de 100644 --- a/dockge_cli/client/commands/functions.py +++ b/dockge_cli/client/commandprovider/bindings.py @@ -1,50 +1,33 @@ from urllib.parse import urlparse from getpass import getpass -import re -from ...models import Credentials +from ...models.parser import Credentials from ...service import storage -from ...service.connection 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 +from ...service.communicate import DockgeConnection -class FunctionBindings(): - """ - 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 - """ +class ExecutionCommands(): @staticmethod def __setup(): - """ - Creates a connection and logs into Dockge - """ con = DockgeConnection() con.connect_and_login() return con @staticmethod def host(extra_args): - """ - host command binding - """ if len(extra_args) > 0: - mat = re.search(r"((\w+\.)?\w+\.\w+(\/.+)?)", extra_args[0], re.IGNORECASE) - if mat is None: - raise ValueError("Given host did not match regex") - res = urlparse(f"https://{mat[0]}") + res = urlparse(extra_args[0]) if all([res.scheme, res.netloc]): - storage.put("host", mat[0]) + host = extra_args[0].rstrip("/").replace("https://", "").replace("wss://", "") + storage.put("host", host) else: raise ValueError(f"Malformed URL {extra_args[0]}") print(storage.get("host")) @staticmethod def login(extra_args): - """ - 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 @@ -53,85 +36,67 @@ class FunctionBindings(): @staticmethod def logout(_): - """ - logout command binding - """ storage.remove("username") storage.remove("password") @staticmethod def exit(_): - """ - exit command binding - """ storage.clear() @staticmethod def list(_): - """ - list command binding - """ - con = FunctionBindings.__setup() + con = ExecutionCommands.__setup() stack_formatter(con.list_stacks()) con.disconnect() @staticmethod def status(extra_args): - """ - 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() @staticmethod def restart(extra_args): - """ - 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() @staticmethod def update(extra_args): - """ - 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() @staticmethod def stop(extra_args): - """ - 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() @staticmethod def start(extra_args): - """ - 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() @staticmethod def down(extra_args): - """ - 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() @staticmethod def help(): - """ - exit command binding - This should never be invoked - """ print("WTF") diff --git a/dockge_cli/client/commandprovider/descriptors.py b/dockge_cli/client/commandprovider/descriptors.py new file mode 100644 index 0000000..cd346fd --- /dev/null +++ b/dockge_cli/client/commandprovider/descriptors.py @@ -0,0 +1,88 @@ +from .bindings import ExecutionCommands + +command_mappings = [ + { + "command": "host", + "description": "Sets and gets the URI of the dockge instance. Remove any unnecessary subdomains/protocols from the URI", + "args": 1, + "optional": True, + "binding": ExecutionCommands.host + }, + { + "command": "login", + "description": "Logs into a given dockge account, either with an interactive dialogue or by passing --user and --password", + "args": 2, + "optional": True, + "binding": ExecutionCommands.login + }, + { + "command": "logout", + "description": "Removes the credentials from the local storage.", + "args": 0, + "optional": False, + "binding": ExecutionCommands.logout + }, + { + "command": "list", + "description": "Lists all available stacks with their status", + "args": 0, + "optional": False, + "binding": ExecutionCommands.list + }, + { + "command": "status", + "description": "Returns the status of one stack", + "args": 1, + "optional": False, + "binding": ExecutionCommands.status + }, + { + "command": "restart", + "description": "Restarts a given stack", + "args": 1, + "optional": False, + "binding": ExecutionCommands.restart + }, + { + "command": "start", + "description": "Starts a given stack", + "args": 1, + "optional": False, + "binding": ExecutionCommands.start + }, + { + "command": "stop", + "description": "Stops a given stack", + "args": 1, + "optional": False, + "binding": ExecutionCommands.stop + }, + { + "command": "down", + "description": "Stop & Downs a given stack", + "args": 1, + "optional": False, + "binding": ExecutionCommands.down + }, + { + "command": "update", + "description": "Updates a stack", + "args": 1, + "optional": False, + "binding": ExecutionCommands.update + }, + { + "command": "exit", + "description": "Exits the CLI - this will reset all settings, including credentials and host", + "args": 0, + "optional": False, + "binding": ExecutionCommands.exit + }, + { + "command": "help", + "description": "Displays helping hints for commands", + "args": 1, + "optional": True, + "binding": ExecutionCommands.help + } +] diff --git a/dockge_cli/client/commandprovider/factory.py b/dockge_cli/client/commandprovider/factory.py new file mode 100644 index 0000000..4bfe95a --- /dev/null +++ b/dockge_cli/client/commandprovider/factory.py @@ -0,0 +1,17 @@ +from typing import List, Callable +from pydantic import BaseModel +from .descriptors import command_mappings + +class Command(BaseModel): + command: str + description: str + args: int + optional: bool + binding: Callable + +commands: dict[str, Command] = {} +descriptors: List[dict[str, object]] = command_mappings + +for descriptor in descriptors: + c = Command(**descriptor) # type: ignore + commands.update({ c.command: c }) diff --git a/dockge_cli/client/commands/__init__.py b/dockge_cli/client/commands/__init__.py deleted file mode 100644 index 61eb6fc..0000000 --- a/dockge_cli/client/commands/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from ...models import Command -from .mappings import mapping - -commands: dict[str, Command] = { c.cmd: c for c in mapping } diff --git a/dockge_cli/client/commands/mappings.py b/dockge_cli/client/commands/mappings.py deleted file mode 100644 index a6ca52f..0000000 --- a/dockge_cli/client/commands/mappings.py +++ /dev/null @@ -1,90 +0,0 @@ -from typing import List -from ...models import Command -from .functions import FunctionBindings - -mapping: List[Command] = [ - Command( - cmd="host", - 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 - ), - 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 - ), - Command( - cmd="logout", - description="Removes the credentials from the local storage.", - args=0, - optional=False, - func=FunctionBindings.logout - ), - Command( - cmd="list", - description="Lists all available stacks with their status", - args=0, - optional=False, - func=FunctionBindings.list - ), - Command( - cmd="status", - description="Returns the status of one stack", - args=1, - optional=False, - func=FunctionBindings.status - ), - Command( - cmd="restart", - description="Restarts a given stack", - args=1, - optional=False, - func=FunctionBindings.restart - ), - Command( - cmd="start", - description="Starts a given stack", - args=1, - optional=False, - func=FunctionBindings.start - ), - Command( - cmd="stop", - description="Stops a given stack", - args=1, - optional=False, - func=FunctionBindings.stop - ), - Command( - cmd="down", - description="Stop & Downs a given stack", - args=1, - optional=False, - func=FunctionBindings.down - ), - Command( - cmd="update", - description="Updates a stack", - args=1, - optional=False, - func=FunctionBindings.update - ), - Command( - cmd="exit", - description="Exits the CLI - this will reset all settings, including credentials and host", - args=0, - optional=False, - func=FunctionBindings.exit - ), - Command( - cmd="help", - description="Displays helping hints for commands", - args=1, - optional=True, - func=FunctionBindings.help - ) -] diff --git a/dockge_cli/client/parser.py b/dockge_cli/client/parser.py index f84628b..2efbc30 100644 --- a/dockge_cli/client/parser.py +++ b/dockge_cli/client/parser.py @@ -2,14 +2,10 @@ import argparse import sys from .. import __version__ -from ..models import Arguments -from .commands import commands +from ..models.parser import Arguments +from .commandprovider.factory import commands def parse_arguments(): - """ - Create a parser and parse the arguments of sys.argv based on the Arguments Namespace - Returns arguments and extra arguments separately - """ parser = argparse.ArgumentParser( prog="dockge_cli", description="CLI interface for interacting with Dockge",) diff --git a/dockge_cli/client/run.py b/dockge_cli/client/run.py index 22d0a2a..105f380 100644 --- a/dockge_cli/client/run.py +++ b/dockge_cli/client/run.py @@ -1,9 +1,6 @@ -from .commands import commands +from .commandprovider.factory import commands def display_help(extra_args): - """ - Display help dialogues for each command - """ if not extra_args: print(f"{commands['help'].description}") return @@ -14,10 +11,6 @@ def display_help(extra_args): print(f"{commands[extra_args[0]].description}") def run(command, args): - """ - Runs a given command with the provided args - Alsso automatically maps the given command string to the correct Command class - """ if command not in commands: raise ValueError("Invalid Command") @@ -32,4 +25,4 @@ def run(command, args): display_help(args) return - c.func(args) + c.binding(args) diff --git a/dockge_cli/client/utils.py b/dockge_cli/client/utils.py index ebddf26..6e9fa87 100644 --- a/dockge_cli/client/utils.py +++ b/dockge_cli/client/utils.py @@ -2,10 +2,7 @@ import argparse from tabulate import tabulate from ..models import StackStatus -def credential_parser_factory(): - """ - Creates a new parser for login credentials - """ +def get_credential_parser(): credentialparser = argparse.ArgumentParser( prog="login", description="Subparser for login credentials provided by CI" @@ -15,30 +12,21 @@ def credential_parser_factory(): return credentialparser def stack_formatter(stacks): - """ - Prints a given stack list formatted as a table - """ if not stacks["ok"]: raise RuntimeError("Stack GET didn't work") 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") def status_formatter(status): - """ - Prints the status for a given stack - """ print(f"Is Stack Ok? {'Yes' if status['ok'] else 'No'}") headers = ["Container", "Status"] table = [[k, v] for k, v in status["serviceStatusList"].items()] print(tabulate(table, headers=headers, tablefmt="github"), "\n") def generic_formatter(status): - """ - Prints a generic dockge message - """ print(f"Is Ok? {'Yes' if status['ok'] else 'No'}") print(f"Stack status: {status['msg']}") diff --git a/dockge_cli/dockge_cli.py b/dockge_cli/dockge_cli.py index af77054..25c437f 100644 --- a/dockge_cli/dockge_cli.py +++ b/dockge_cli/dockge_cli.py @@ -2,8 +2,5 @@ from .client.parser import parse_arguments from .client.run import run def cli(): - """ - main function for cli invocation - """ command, args= parse_arguments() run(command.command, args) diff --git a/dockge_cli/models/__init__.py b/dockge_cli/models/__init__.py index ac7af80..ef15c9b 100644 --- a/dockge_cli/models/__init__.py +++ b/dockge_cli/models/__init__.py @@ -1,3 +1 @@ from .codes import StackStatus -from .command import Command -from .parser import Arguments, Credentials diff --git a/dockge_cli/models/codes.py b/dockge_cli/models/codes.py index 569ce1d..b8f9a07 100644 --- a/dockge_cli/models/codes.py +++ b/dockge_cli/models/codes.py @@ -1,9 +1,7 @@ from enum import Enum class StackStatus(Enum): - """ - mapping for plaintext vs statuscode - """ - INACTIVE = 1 - RUNNING = 3 - EXITED = 4 + # pylint: disable=invalid-name + inactive = 1 + running = 3 + exited = 4 diff --git a/dockge_cli/models/command.py b/dockge_cli/models/command.py deleted file mode 100644 index 61a13b3..0000000 --- a/dockge_cli/models/command.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Callable -from pydantic import BaseModel - -class Command(BaseModel): - """ - Basic command structure for the CLI to automatically generate valid commands - """ - cmd: str - func: Callable - args: int - optional: bool - description: str diff --git a/dockge_cli/models/parser.py b/dockge_cli/models/parser.py index e590ce9..c836e62 100644 --- a/dockge_cli/models/parser.py +++ b/dockge_cli/models/parser.py @@ -2,15 +2,9 @@ import argparse # pylint: disable=too-few-public-methods class Arguments(argparse.Namespace): - """ - Default Arguments when calling the CLI - """ command: str # pylint: disable=too-few-public-methods class Credentials(argparse.Namespace): - """ - Special Argument Namespace for login credentials of the login commands - """ username: str password: str diff --git a/dockge_cli/service/.gitignore b/dockge_cli/service/.gitignore deleted file mode 100644 index 9e62042..0000000 --- a/dockge_cli/service/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.storage/* \ No newline at end of file diff --git a/dockge_cli/service/connection.py b/dockge_cli/service/communicate.py similarity index 69% rename from dockge_cli/service/connection.py rename to dockge_cli/service/communicate.py index 78e7582..4eb645d 100644 --- a/dockge_cli/service/connection.py +++ b/dockge_cli/service/communicate.py @@ -5,14 +5,8 @@ import socketio.exceptions from . import storage class DockgeConnection: - """ - Provider class for Dockge - Provides all the functionality for connecting, logging in and executing commands - """ class LoginException(BaseException): - """ - Special exception when login fails too often - """ + pass _sio: socketio.Client _host: str @@ -32,7 +26,7 @@ class DockgeConnection: def _init_events(self): @self._sio.event def connect(): - self.login() + self.connect() print("Connected!") @self._sio.event @@ -63,34 +57,20 @@ class DockgeConnection: success = True else: print("Issue with login procedure") - print(data) return success # Functions def connect_and_login(self): - """ - 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/") - self.login() + self._sio.connect(f"https://{self._host}/socket.io/", transports=['websocket']) + self.connect() - def login(self): - """ - Log into dockge using basicauth - Retries 5 times when timeouts occur - """ + def connect(self): if self._logged_in: return 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( @@ -98,13 +78,12 @@ class DockgeConnection: { "username": storage.get("username", encoded=True), "password": storage.get("password", encoded=True), - "token": "" + "token":"" }, - timeout=10 + timeout=5 ) retry = False except socketio.exceptions.TimeoutError: - print("Reached timeout for login, retrying ...") retry = True count += 1 @@ -113,9 +92,6 @@ class DockgeConnection: self._logged_in = True def list_stacks(self): - """ - Requests stack list from dockge, returns list when event was sent - """ self._sio.emit("agent", ("", "requestStackList")) while self._stacklist is None: time.sleep(0.5) @@ -124,50 +100,29 @@ class DockgeConnection: return retval def list_stack(self, name: str): - """ - Lists status for a stack - """ - ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=10) + ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=5) return ret def restart(self, name): - """ - Restarts a given stack - """ - ret = self._sio.call("agent", ("", "restartStack", name), timeout=30) + ret = self._sio.call("agent", ("", "restartStack", name), timeout=10) return ret def update(self, name): - """ - Updates a given stack - """ - ret = self._sio.call("agent", ("", "updateStack", name), timeout=30) + ret = self._sio.call("agent", ("", "updateStack", name), timeout=10) return ret def stop(self, name): - """ - Stops a given stack - """ - ret = self._sio.call("agent", ("", "stopStack", name), timeout=30) + ret = self._sio.call("agent", ("", "stopStack", name), timeout=10) return ret def start(self, name): - """ - Starts a given stack - """ - ret = self._sio.call("agent", ("", "startStack", name), timeout=30) + ret = self._sio.call("agent", ("", "startStack", name), timeout=10) return ret def down(self, name): - """ - Stops and downs a given stack - """ - ret = self._sio.call("agent", ("", "downStack", name), timeout=30) + ret = self._sio.call("agent", ("", "downStack", name), timeout=10) return ret def disconnect(self): - """ - Logs out of dockge - """ self._sio.emit("logout") self._sio.disconnect() diff --git a/dockge_cli/service/storage.py b/dockge_cli/service/storage.py index 3edf415..0a3a0f8 100644 --- a/dockge_cli/service/storage.py +++ b/dockge_cli/service/storage.py @@ -3,82 +3,42 @@ 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(): - """ - Checks if storage file does exist, creates it when necessary - """ - if _file.exists(): - return - with open(_file, 'a', encoding="utf-8"): - os.utime(_file, None) +_storagepath.mkdir(exist_ok=True, parents=True) -def exists(key: str) -> bool: - """ - Checks if a given key exists in the storage file - """ +def fileexists(): 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 - + with open(_file, 'a', encoding="utf-8"): + os.utime(_file, None) 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 }) - + content.update({ key: str(base64.b64encode(value.encode())) if encoded else value }) with open(_file, "w+", encoding="utf-8") as file: yaml.dump(content, file, Dumper=yaml.SafeDumper) 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) def get(key: str, encoded=False): - """ - Retrieves a value for a given key from the storage file - If the value was encoded, encoded needs to be set True to decode it again - """ 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 + return base64.b64decode(value).decode() if encoded else value def clear(): - """ - Deletes the storage file - """ - if not _file.exists(): - return _file.unlink() diff --git a/pyproject.toml b/pyproject.toml index b706b37..50b898b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,9 @@ [project] name = "dockge_cli" -version = "0.1.2" +version = "0.0.1-c.1" dependencies = [ "pyyaml~=6.0.1", "pydantic~=2.8.0", - "requests~=2.32.3", "python-socketio~=5.11.3", "websocket-client~=1.8.0", "tabulate ~=0.9.0", @@ -12,7 +11,7 @@ dependencies = [ requires-python = ">= 3.10" authors = [{name = "Firq", email = "firelp42@gmail.com"}] maintainers = [{name = "Firq", email = "firelp42@gmail.com"}] -description = "CLI for interacting with dockge" +description = "CLi for interacting with dockge" classifiers = [ "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3", @@ -32,30 +31,8 @@ include = ["dockge_cli*"] [tool.setuptools.package-data] "*" = ["py.typed"] -[project.optional-dependencies] -lint = [ - "pylint~=3.2.5", -] -typing = [ - "mypy~=1.10.1", - "types-PyYAML~=6.0.12.20240311", - "types-tabulate~=0.9.0.20240106", -] - [tool.pylint."MAIN"] -disable = [ - "line-too-long", - "missing-module-docstring", -] - -[tool.mypy] -python_version = "3.11" -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = 'socketio.*' -ignore_missing_imports = true +disable = [ "line-too-long", "missing-module-docstring", "missing-function-docstring", "missing-class-docstring" ] [build-system] requires = ["setuptools >= 61.0"]