diff --git a/.gitignore b/.gitignore
index 880101f..a78f7fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ __pycache__
.env
duckington_cli.egg-info
.coverage
+.vscode
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..a77ed1d
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,32 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+default_language_version:
+ python: python3.11
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: check-added-large-files
+ - id: check-toml
+ - id: check-yaml
+ args:
+ - --unsafe
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.7.4
+ hooks:
+ - id: ruff
+ args:
+ - --fix
+ - id: ruff-format
+- repo: https://github.com/igorshubovych/markdownlint-cli
+ rev: v0.41.0
+ hooks:
+ - id: markdownlint
+ args: [ --disable=MD013, --disable=MD002,--disable=MD032,--disable=MD005,--disable=MD009 ]
+
+
+ci:
+ autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
+ autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate
diff --git a/README.md b/README.md
index 63849e6..83decea 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,21 @@
-# Command Line Interface (CLI)
+# Duckington CLI
-The CLI is built using [click](https://click.palletsprojects.com/en/8.1.x/). Use the following command to get started with the CLI:
+
+
+

+
+
+The CLI is built using [click](https://click.palletsprojects.com/en/8.1.x/).
+
+Use the following command to get started with the CLI:
```bash
quack
```
+## Contributing
+
+To get started contributing to this project see the [setup](docs/setup.md) page
## Testing
@@ -20,51 +30,3 @@ or
```bash
python3 -m pytest
```
-
-## Setup
-
-Follow these steps to set up a Python virtual environment using `venv`
-
-### 1. Create Virtual Environment
-
-```bash
-python3 -m venv .venv
-```
-
-### 2. Activate Virtual Environment
-
-- Linux / macOS:
-
-```bash
-source .venv/bin/activate
-```
-
-- Windows:
-
-```bash
-.venv\Scripts\activate
-```
-
-- Git Bash:
-
-```bash
-source .venv/Scripts/activate
-```
-
-### 3. Install Dependencies
-
-```bash
-pip install -r requirements.txt
-```
-
-### 4. Install CLI application in editable mode
-
-```bash
-pip install --editable .
-```
-
-### 5. To deactivate the venv
-
-```bash
-deactivate
-```
diff --git a/docs/assets/duckington.png b/docs/assets/duckington.png
new file mode 100644
index 0000000..468367b
Binary files /dev/null and b/docs/assets/duckington.png differ
diff --git a/docs/setup.md b/docs/setup.md
new file mode 100644
index 0000000..bc7db2e
--- /dev/null
+++ b/docs/setup.md
@@ -0,0 +1,61 @@
+# Setup
+
+Follow these steps to get started with `quack`.
+
+## TL;DR
+
+1. Set up a Python virtual environment using `venv`
+2. Install Pre-commit hooks
+3. Install `quack` as an editable module
+
+## Environment Setup
+
+### 1. Create Virtual Environment
+
+```bash
+python3 -m venv .venv
+```
+
+### 2. Activate Virtual Environment
+
+- **Linux / macOS:**
+
+ ```bash
+ source .venv/bin/activate
+ ```
+
+- **Windows:**
+
+ ```bash
+ . .venv\Scripts\activate
+ ```
+
+- **Git Bash:**
+
+ ```bash
+ source .venv/Scripts/activate
+ ```
+
+### 3. Install Dependencies
+
+```bash
+pip install -r requirements.txt
+```
+
+### 4. Activate Pre-commit Hooks
+
+```bash
+pre-commit install
+```
+
+### 5. Install CLI Application in Editable Mode
+
+```bash
+pip install --editable .
+```
+
+### 6. Deactivate the Virtual Environment
+
+```bash
+deactivate
+```
diff --git a/requirements.txt b/requirements.txt
index ba5e5f9..f251454 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/src/api/api_client.py b/src/api/api_client.py
index 1c3ce26..d28f06e 100644
--- a/src/api/api_client.py
+++ b/src/api/api_client.py
@@ -1,3 +1,4 @@
+from io import BufferedReader
import requests
from typing import Optional, Dict, Any
from src.config.settings import API_BASE_URL
@@ -21,6 +22,7 @@ def _make_request(
data: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
+ files: Optional[Dict[str, BufferedReader]] = None,
) -> Dict[str, Any]:
url: str = f"{self.base_url}/{endpoint}"
headers = headers or {}
@@ -37,6 +39,7 @@ def _make_request(
data=data if endpoint == "auth" else None,
params=params,
headers=headers,
+ files=files,
)
response.raise_for_status()
return response.json()
@@ -55,9 +58,10 @@ def post(
data: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
+ files: Optional[Dict[str, BufferedReader]] = None,
) -> Dict[str, Any]:
return self._make_request(
- "POST", endpoint, data=data, params=params, headers=headers
+ "POST", endpoint, data=data, params=params, headers=headers, files=files
)
def put(
@@ -66,9 +70,10 @@ def put(
data: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
+ files: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
return self._make_request(
- "PUT", endpoint, data=data, params=params, headers=headers
+ "PUT", endpoint, data=data, params=params, headers=headers, files=files
)
def delete(
diff --git a/src/api/model_file_api.py b/src/api/model_file_api.py
new file mode 100644
index 0000000..fcb4f18
--- /dev/null
+++ b/src/api/model_file_api.py
@@ -0,0 +1,52 @@
+import os
+from typing import Dict
+
+from src.api.api_client import APIClient
+from src.utils.helpers.handle_api_errors import handle_api_errors
+
+
+class ModelFileAPI:
+ def __init__(self, client: APIClient):
+ self.client = client
+
+ @handle_api_errors
+ def upload_model_file(
+ self, model_name: str, model_id: str, file_path: str
+ ) -> Dict[str, str]:
+ with open(file_path, "rb") as file:
+ files = {
+ "file": (os.path.basename(file_path), file, "application/octet-stream")
+ }
+ params = {"model_name": model_name, "model_id": model_id}
+ return self.client.post("models", params=params, files=files)
+
+ @handle_api_errors
+ def get_model(self, model_id: str):
+ return self.client.get(f"models/{model_id}")
+
+ @handle_api_errors
+ def get_all_models(self):
+ return self.client.get("models")
+
+ @handle_api_errors
+ def read_model_file(self, model_id: str, file_name: str):
+ return self.client.get(f"models/{model_id}/{file_name}")
+
+ @handle_api_errors
+ def update_model_file(
+ self, model_name: str, model_id: str, file_path: str
+ ) -> Dict[str, str]:
+ with open(file_path, "rb") as file:
+ files = {
+ "file": (os.path.basename(file_path), file, "application/octet-stream")
+ }
+ params = {"model_name": model_name, "model_id": model_id}
+ return self.client.put("models", params=params, files=files)
+
+ @handle_api_errors
+ def delete_model_file(self, model_id: str, file_name: str):
+ return self.client.delete(f"models/{model_id}/{file_name}")
+
+ @handle_api_errors
+ def delete_model(self, model_id: str):
+ return self.client.delete(f"models/{model_id}")
diff --git a/src/commands/__init__.py b/src/commands/__init__.py
index e69de29..05dc52b 100644
--- a/src/commands/__init__.py
+++ b/src/commands/__init__.py
@@ -0,0 +1,7 @@
+from src.commands import key
+from src.commands import user_auth
+from src.commands import machine
+from src.commands import metrics
+from src.commands import model_file
+
+__all__ = [key, user_auth, machine, metrics, model_file]
diff --git a/src/commands/auth_commands.py b/src/commands/auth_commands.py
deleted file mode 100644
index bb1beee..0000000
--- a/src/commands/auth_commands.py
+++ /dev/null
@@ -1,147 +0,0 @@
-import click
-from src.commands.base_command import BaseCommand
-from src.api.api_client import APIClient
-from src.api.auth_api import AuthAPI
-from src.utils.helpers.validity_enum import ValidityEnum
-
-
-class LoginCommand(BaseCommand):
- def execute(self, email, password):
- client = APIClient()
- endpoint = AuthAPI(client)
- result = endpoint.login(email, password)
- if result["success"]:
- print(f"Successfully logged in. {result['data']}")
- else:
- print(f"Login failed. {result['message']}")
-
-
-@click.command()
-@click.option("--email", prompt=True)
-@click.option("--password", prompt=True, hide_input=True)
-@click.pass_context
-def login(ctx, email, password):
- cmd = LoginCommand()
- cmd.execute(email, password)
-
-
-class LogoutCommand(BaseCommand):
- def execute(self):
- client = APIClient()
- endpoint = AuthAPI(client)
- logout_result = endpoint.logout()
-
- if logout_result:
- print("Successfully logged out.")
- else:
- print("No action taken. You were not logged in.")
-
- return logout_result
-
-
-@click.command()
-@click.pass_context
-def logout(ctx):
- cmd = LogoutCommand()
- cmd.execute()
-
-
-class CreateAPIKeyCommand(BaseCommand):
- def execute(self, validity):
- client = APIClient()
- endpoint = AuthAPI(client)
- result = endpoint.create_api_key(ValidityEnum[validity])
- if result["success"]:
- print(f"API key created successfully: {result['data']}")
- else:
- print(f"Failed to create API key. {result['message']}")
-
-
-@click.command()
-@click.option(
- "--validity",
- type=click.Choice(["ONE_HOUR", "ONE_DAY", "ONE_WEEK", "ONE_MONTH", "ONE_YEAR"]),
- default="ONE_DAY",
- show_default=True,
- prompt="Choose validity period",
-)
-@click.pass_context
-def create_api_key(ctx, validity):
- cmd = CreateAPIKeyCommand()
- cmd.execute(validity)
-
-
-class ListAPIKeysCommand(BaseCommand):
- def execute(self):
- client = APIClient()
- endpoint = AuthAPI(client)
- result = endpoint.list_api_keys()
- if result["success"]:
- print("API Keys:")
- for key in result["data"]:
- print(
- f"Token: {key['token']}, Created At: {key['created_at']}, Validity: {key['validity']}"
- )
- else:
- print(f"Failed to retrieve API keys. {result['message']}")
-
-
-@click.command()
-@click.pass_context
-def list_api_keys(ctx):
- cmd = ListAPIKeysCommand()
- cmd.execute()
-
-
-class DeleteAPIKeyCommand(BaseCommand):
- def execute(self, token):
- client = APIClient()
- endpoint = AuthAPI(client)
- result = endpoint.delete_api_key(token)
- if result["success"]:
- print(f"API key deleted successfully. {result['data']}")
- else:
- print(f"Failed to delete API key. {result['message']}")
-
-
-@click.command()
-@click.option("--token", prompt=True)
-@click.pass_context
-def delete_api_key(ctx, token):
- cmd = DeleteAPIKeyCommand()
- cmd.execute(token)
-
-
-class SetAPIKeyCommand(BaseCommand):
- def execute(self, api_key):
- client = APIClient()
- endpoint = AuthAPI(client)
- result = endpoint.set_api_key(api_key)
- if result:
- print("API key set successfully.")
-
-
-@click.command()
-@click.option("--api-key", prompt=True, hide_input=True)
-@click.pass_context
-def set_api_key(ctx, api_key):
- cmd = SetAPIKeyCommand()
- cmd.execute(api_key)
-
-
-class RemoveAPIKeyCommand(BaseCommand):
- def execute(self):
- client = APIClient()
- endpoint = AuthAPI(client)
- result = endpoint.clear_api_key()
- if result:
- print("API key removed successfully.")
- else:
- print("No API key was set.")
-
-
-@click.command()
-@click.pass_context
-def remove_api_key(ctx):
- cmd = RemoveAPIKeyCommand()
- cmd.execute()
diff --git a/src/commands/base_command.py b/src/commands/base_command.py
deleted file mode 100644
index b1f0887..0000000
--- a/src/commands/base_command.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from abc import ABC, abstractmethod
-
-class BaseCommand(ABC):
- @abstractmethod
- def execute(self, *args, **kwargs):
- """Execute the command"""
- pass
-
diff --git a/src/commands/key.py b/src/commands/key.py
new file mode 100644
index 0000000..ba549d3
--- /dev/null
+++ b/src/commands/key.py
@@ -0,0 +1,100 @@
+import click
+
+from src.api.api_client import APIClient
+from src.api.auth_api import AuthAPI
+from src.utils.groups.subcommand_group import SubCommandGroup
+from src.utils.helpers.validity_enum import ValidityEnum
+
+
+class KeyCommands:
+ def __init__(self):
+ self.client = APIClient()
+ self.endpoint = AuthAPI(self.client)
+
+ def create_api_key(self, validity):
+ result = self.endpoint.create_api_key(ValidityEnum[validity])
+ if result["success"]:
+ click.echo(f"API key created successfully: {result['data']}")
+ else:
+ click.echo(f"Failed to create API key. {result['message']}")
+
+ def list_api_keys(self):
+ result = self.endpoint.list_api_keys()
+ if result["success"]:
+ click.echo("API Keys:")
+ for key in result["data"]:
+ click.echo(
+ f"Token: {key['token']}, Created At: {key['created_at']}, Validity: {key['validity']}"
+ )
+ else:
+ click.echo(f"Failed to retrieve API keys. {result['message']}")
+
+ def delete_api_key(self, token):
+ result = self.endpoint.delete_api_key(token)
+ if result["success"]:
+ click.echo(f"API key deleted successfully. {result['data']}")
+ else:
+ click.echo(f"Failed to delete API key. {result['message']}")
+
+ def set_api_key(self, api_key):
+ result = self.endpoint.set_api_key(api_key)
+ if result:
+ click.echo("API key set successfully.")
+
+ def remove_api_key(self):
+ result = self.endpoint.clear_api_key()
+ if result:
+ click.echo("API key removed successfully.")
+ else:
+ click.echo("No API key was set.")
+
+
+@click.group(cls=SubCommandGroup)
+@click.pass_context
+def key(ctx):
+ """API key management commands."""
+ ctx.obj = KeyCommands()
+
+
+@key.command()
+@click.option(
+ "--validity",
+ type=click.Choice(["ONE_HOUR", "ONE_DAY", "ONE_WEEK", "ONE_MONTH", "ONE_YEAR"]),
+ default="ONE_DAY",
+ show_default=True,
+ prompt="Choose validity period",
+)
+@click.pass_context
+def create(ctx, validity):
+ """Create a new API key."""
+ ctx.obj.create_api_key(validity)
+
+
+@key.command()
+@click.pass_context
+def list(ctx):
+ """List all API keys."""
+ ctx.obj.list_api_keys()
+
+
+@key.command()
+@click.option("--token", prompt=True)
+@click.pass_context
+def delete(ctx, token):
+ """Delete an API key."""
+ ctx.obj.delete_api_key(token)
+
+
+@key.command()
+@click.option("--api-key", prompt=True, hide_input=True)
+@click.pass_context
+def set(ctx, api_key):
+ """Set an API key."""
+ ctx.obj.set_api_key(api_key)
+
+
+@key.command()
+@click.pass_context
+def unset(ctx):
+ """Remove the set API key."""
+ ctx.obj.remove_api_key()
diff --git a/src/commands/machine_commands.py b/src/commands/machine.py
similarity index 60%
rename from src/commands/machine_commands.py
rename to src/commands/machine.py
index c131658..3f83d97 100644
--- a/src/commands/machine_commands.py
+++ b/src/commands/machine.py
@@ -1,14 +1,16 @@
import click
from src.api.machine_api import MachineAPI
-from src.commands.base_command import BaseCommand
+from src.utils.groups.subcommand_group import SubCommandGroup
-class CreateMachineCommand(BaseCommand):
- def execute(self, machine_name, machine_type):
- endpoint = MachineAPI()
+class MachineCommands:
+ def __init__(self):
+ self.endpoint = MachineAPI()
+
+ def create(self, machine_name, machine_type):
click.echo("\nAttempting to create machine...\n")
- result = endpoint.create_machine(machine_name, machine_type)
+ result = self.endpoint.create_machine(machine_name, machine_type)
if result["success"]:
click.echo(f"Machine created successfully. Details: {result['data']}")
else:
@@ -16,35 +18,12 @@ def execute(self, machine_name, machine_type):
"Failed to create machine. Check machine name and type and try again."
)
-
-@click.command()
-@click.pass_context
-@click.option("--machine-name", "-n", required=True, prompt="Enter machine name")
-@click.option(
- "--machine-type",
- "-t",
- required=True,
- prompt="Choose machine type",
- type=click.Choice(["t2.micro", "f1.2xlarge", "f1.4xlarge", "f1.16xlarge"]),
- default="f1.2xlarge",
- show_default=True,
-)
-def create_machine(ctx, machine_name, machine_type):
- """Create a machine with name and type."""
- cmd = CreateMachineCommand()
- cmd.execute(machine_name=machine_name, machine_type=machine_type)
-
-
-class ListMachinesCommand(BaseCommand):
- def execute(self):
- endpoint = MachineAPI()
- result = endpoint.list_user_machines()
-
+ def list(self):
+ result = self.endpoint.list_user_machines()
if result["success"]:
if not result["data"]:
click.echo("No machines to list.")
return
-
click.echo("Machines:")
for machine in result["data"]:
click.echo(machine)
@@ -52,99 +31,98 @@ def execute(self):
else:
click.echo("Failed to retrieve list of machines.")
-
-@click.command()
-@click.pass_context
-def list_machines(ctx):
- """List all machines assigned to user."""
- cmd = ListMachinesCommand()
- cmd.execute()
-
-
-class StopMachine(BaseCommand):
- def execute(self, machine_id):
- endpoint = MachineAPI()
-
+ def stop(self, machine_id):
click.echo("\nAttempting to stop machine...\n")
- result = endpoint.stop_machine(machine_id)
-
+ result = self.endpoint.stop_machine(machine_id)
if result["success"]:
click.echo(f"{result['data']['message']}")
else:
click.echo("Failed to stop machine. Check machine ID and try again.")
-
-@click.command()
-@click.option("--machine-id", "-id", required=True, prompt="Enter machine ID")
-@click.pass_context
-def stop_machine(ctx, machine_id):
- """Stop machine with machine ID."""
- cmd = StopMachine()
- cmd.execute(machine_id)
-
-
-class StartMachine(BaseCommand):
- def execute(self, machine_id):
- endpoint = MachineAPI()
-
+ def start(self, machine_id):
click.echo("\nAttempting to start machine...\n")
- result = endpoint.start_machine(machine_id)
-
+ result = self.endpoint.start_machine(machine_id)
if result["success"]:
click.echo(f"{result['data']['message']}")
else:
click.echo("Failed to start machine. Check machine ID and try again.")
+ def terminate(self, machine_id):
+ click.echo("\nAttempting to terminate machine...\n")
+ result = self.endpoint.terminate_machine(machine_id)
+ if result["success"]:
+ click.echo(f"{result['data']['message']}")
+ else:
+ click.echo("Failed to terminate machine. Check machine ID and try again.")
-@click.command()
-@click.option("--machine-id", "-id", required=True, prompt="Enter machine ID")
+ def get_details(self, machine_id):
+ result = self.endpoint.get_machine(machine_id)
+ if result["success"]:
+ click.echo(f"Machine {machine_id} details:")
+ click.echo(result["data"])
+ return result["data"]
+ else:
+ click.echo("Failed to get machine. Check machine ID and try again.")
+
+
+@click.group(cls=SubCommandGroup)
@click.pass_context
-def start_machine(ctx, machine_id):
- """Start machine with machine ID."""
- cmd = StartMachine()
- cmd.execute(machine_id)
+def machine(ctx):
+ """Machine management commands."""
+ ctx.obj = MachineCommands()
-class TerminateMachine(BaseCommand):
- def execute(self, machine_id):
- endpoint = MachineAPI()
+@machine.command()
+@click.pass_context
+@click.option("--machine-name", "-n", required=True, prompt="Enter machine name")
+@click.option(
+ "--machine-type",
+ "-t",
+ required=True,
+ prompt="Choose machine type",
+ type=click.Choice(["t2.micro", "f1.2xlarge", "f1.4xlarge", "f1.16xlarge"]),
+ default="f1.2xlarge",
+ show_default=True,
+)
+def create(ctx, machine_name, machine_type):
+ """Create a machine with name and type."""
+ ctx.obj.create(machine_name=machine_name, machine_type=machine_type)
- click.echo("\nAttempting to terminate machine...\n")
- result = endpoint.terminate_machine(machine_id)
- if result["success"]:
- click.echo(f"{result['data']['message']}")
- else:
- click.echo("Failed to terminate machine. Check machine ID and try again.")
+@machine.command()
+@click.pass_context
+def list(ctx):
+ """List all machines assigned to user."""
+ ctx.obj.list()
-@click.command()
+@machine.command()
@click.option("--machine-id", "-id", required=True, prompt="Enter machine ID")
@click.pass_context
-def terminate_machine(ctx, machine_id):
- """Terminate machine with machine ID."""
- cmd = TerminateMachine()
- cmd.execute(machine_id)
+def stop(ctx, machine_id):
+ """Stop machine with machine ID."""
+ ctx.obj.stop(machine_id)
-class GetMachineDetails(BaseCommand):
- def execute(self, machine_id):
- endpoint = MachineAPI()
+@machine.command()
+@click.option("--machine-id", "-id", required=True, prompt="Enter machine ID")
+@click.pass_context
+def start(ctx, machine_id):
+ """Start machine with machine ID."""
+ ctx.obj.start(machine_id)
- result = endpoint.get_machine(machine_id)
- if result["success"]:
- click.echo(f"Machine {machine_id} details:")
- click.echo(result["data"])
- return result["data"]
- else:
- click.echo("Failed to Get machine. Check machine ID and try again.")
+@machine.command()
+@click.option("--machine-id", "-id", required=True, prompt="Enter machine ID")
+@click.pass_context
+def terminate(ctx, machine_id):
+ """Terminate machine with machine ID."""
+ ctx.obj.terminate(machine_id)
-@click.command()
+@machine.command()
@click.option("--machine-id", "-id", required=True, prompt="Enter machine ID")
@click.pass_context
-def get_machine_details(ctx, machine_id):
+def details(ctx, machine_id):
"""Get machine details with machine ID."""
- cmd = GetMachineDetails()
- cmd.execute(machine_id)
+ ctx.obj.get_details(machine_id)
diff --git a/src/commands/metrics.py b/src/commands/metrics.py
new file mode 100644
index 0000000..80b7955
--- /dev/null
+++ b/src/commands/metrics.py
@@ -0,0 +1,16 @@
+import click
+
+from src.utils.groups.subcommand_group import SubCommandGroup
+
+
+class MachineMetricsCommand:
+ """Commands for machine metrics."""
+
+ pass
+
+
+@click.group(cls=SubCommandGroup)
+@click.pass_context
+def metrics(ctx):
+ """Function not yet supported."""
+ ctx.obj = MachineMetricsCommand()
diff --git a/src/commands/metrics_commands.py b/src/commands/metrics_commands.py
deleted file mode 100644
index 88cdd73..0000000
--- a/src/commands/metrics_commands.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import click
-from src.commands.base_command import BaseCommand
-
-
-class MachineMetricsCommand(BaseCommand):
- def execute(self):
- print("Function not yet supported.")
-
-
-@click.command()
-@click.pass_context
-def machine_metrics(ctx):
- """
- This command is not implemented yet.
- """
- cmd = MachineMetricsCommand()
- cmd.execute()
diff --git a/src/commands/model_file.py b/src/commands/model_file.py
new file mode 100644
index 0000000..2f4dac0
--- /dev/null
+++ b/src/commands/model_file.py
@@ -0,0 +1,207 @@
+from datetime import datetime
+
+import click
+
+from src.api.api_client import APIClient
+from src.api.model_file_api import ModelFileAPI
+from src.utils.groups.subcommand_group import SubCommandGroup
+
+
+class ModelFileCommands:
+ def __init__(self):
+ self.client = APIClient()
+ self.endpoint = ModelFileAPI(self.client)
+
+ def upload(
+ self,
+ file_path: str,
+ model_name: str | None = None,
+ model_id: str | None = None,
+ ):
+ click.echo("\nAttempting to upload model file...\n")
+ result = self.endpoint.upload_model_file(model_name, model_id, file_path)
+ if result["success"]:
+ upload_date = datetime.strptime(
+ result["data"]["upload_date"], "%Y-%m-%dT%H:%M:%S.%f%z"
+ )
+ formatted_date = upload_date.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
+ click.echo("Model file uploaded successfully:")
+ click.echo(f" Model Name: {result['data']['model_name']}")
+ click.echo(f" Model ID: {result['data']['model_id']}")
+ click.echo(f" Upload Date: {formatted_date}")
+ else:
+ click.echo(f"Failed to upload model file: {result['response']['detail']}")
+
+ def list(self):
+ result = self.endpoint.get_all_models()
+ if result["success"]:
+ if not result["data"]:
+ click.echo("No models to list.")
+ return
+ click.echo("Models:")
+ for model in result["data"]:
+ click.echo(f" Name: {model['model_name']}")
+ click.echo(f" ID: {model['model_id']}")
+ click.echo(" Files:")
+ for file in model["files"]:
+ last_modified = datetime.strptime(
+ file["last_modified"], "%Y-%m-%dT%H:%M:%SZ"
+ )
+ last_modified = last_modified.astimezone().strftime(
+ "%Y-%m-%d %H:%M:%S %Z"
+ )
+ click.echo(
+ f" • {file['file_name']} ({file['file_size']} bytes)\n\tlast modified: {last_modified}"
+ )
+ else:
+ click.echo(f"Failed to list models: {result['response']['detail']}")
+
+ def get_model(self, model_id):
+ result = self.endpoint.get_model(model_id)
+ if result["success"]:
+ click.echo(f"Name: {result['data']['model_name']}")
+ click.echo(f"ID: {result['data']['model_id']}")
+ click.echo("Files:")
+ for file in result["data"]["files"]:
+ last_modified = datetime.strptime(
+ file["last_modified"], "%Y-%m-%dT%H:%M:%SZ"
+ )
+ last_modified = last_modified.astimezone().strftime(
+ "%Y-%m-%d %H:%M:%S %Z"
+ )
+ click.echo(f" • {file['file_name']} ({file['file_size']} bytes)")
+ click.echo(f" last modified: {last_modified}")
+ else:
+ click.echo(f"Failed to get model. {result['response']['detail']}")
+
+ def read_file(self, model_id, file_name):
+ result = self.endpoint.read_model_file(model_id, file_name)
+ if result["success"]:
+ click.echo(f"File contents for {file_name}:")
+ click.echo(result["data"]["content"])
+ else:
+ click.echo(f"Failed to read model file. {result['response']['detail']}")
+
+ def update(
+ self,
+ file_path: str,
+ model_name: str | None = None,
+ model_id: str | None = None,
+ ):
+ click.echo("\nAttempting to update model file...\n")
+ result = self.endpoint.update_model_file(
+ model_name=model_name, model_id=model_id, file_path=file_path
+ )
+ if result["success"]:
+ upload_date = datetime.strptime(
+ result["data"]["upload_date"], "%Y-%m-%dT%H:%M:%S.%f%z"
+ )
+ formatted_date = upload_date.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
+ click.echo("Model file uploaded successfully:")
+ click.echo(f" Model Name: {result['data']['model_name']}")
+ click.echo(f" Model ID: {result['data']['model_id']}")
+ click.echo(f" Upload Date: {formatted_date}")
+ else:
+ click.echo(f"Failed to upload model file: {result['response']['detail']}")
+
+ def delete_file(self, model_id, file_name):
+ click.echo("\nAttempting to delete model file...\n")
+ result = self.endpoint.delete_model_file(model_id, file_name)
+ if result["success"] and result["data"] is None:
+ click.echo(f"Model file {file_name} deleted successfully")
+ else:
+ click.echo(f"Failed to delete model file. {result['response']['detail']}")
+
+ def delete_model(self, model_id):
+ click.echo("\nAttempting to delete model...\n")
+ result = self.endpoint.delete_model(model_id)
+ if result["success"] and result["data"] is None:
+ click.echo(f"Model {model_id} deleted successfully")
+ else:
+ click.echo(f"Failed to delete model. {result['response']['detail']}")
+
+
+@click.group(cls=SubCommandGroup)
+@click.pass_context
+def model(ctx):
+ """Model file management commands."""
+ ctx.obj = ModelFileCommands()
+
+
+@model.command()
+@click.pass_context
+@click.option("--model-name", "-n", required=False)
+@click.option("--model-id", "-id", required=False)
+@click.option(
+ "--file-path",
+ "-f",
+ required=True,
+ prompt="Enter file path",
+ type=click.Path(exists=True),
+ shell_complete=click.Path().shell_complete,
+)
+def upload(ctx, model_name, model_id, file_path):
+ """Upload a model file."""
+ if not model_name and not model_id:
+ model_name = click.prompt("Enter model name")
+ ctx.obj.upload(model_name=model_name, model_id=model_id, file_path=file_path)
+
+
+@model.command()
+@click.pass_context
+def list(ctx):
+ """List all models."""
+ ctx.obj.list()
+
+
+@model.command()
+@click.pass_context
+@click.option("--model-id", "-id", required=True, prompt="Enter model ID")
+def get(ctx, model_id):
+ """Get model details with model ID."""
+ ctx.obj.get_model(model_id)
+
+
+@model.command()
+@click.pass_context
+@click.option("--model-id", "-id", required=True, prompt="Enter model ID")
+@click.option("--file-name", "-f", required=True, prompt="Enter file name")
+def read(ctx, model_id, file_name):
+ """Read contents of a model file."""
+ ctx.obj.read_file(model_id, file_name)
+
+
+@model.command()
+@click.pass_context
+@click.option("--model-name", "-n", required=False)
+@click.option("--model-id", "-id", required=False)
+@click.option(
+ "--file-path",
+ "-f",
+ required=True,
+ prompt="Enter file path",
+ type=click.Path(exists=True),
+ shell_complete=click.Path().shell_complete,
+)
+def update(ctx, model_name, model_id, file_path):
+ """Update a model file."""
+ if not model_name and not model_id:
+ model_name = click.prompt("Enter model name")
+ ctx.obj.update(model_name=model_name, model_id=model_id, file_path=file_path)
+
+
+@model.command()
+@click.pass_context
+@click.option("--model-id", "-id", required=True, prompt="Enter model ID")
+@click.option("--file-name", "-f", required=True, prompt="Enter file name")
+def delete_file(ctx, model_id, file_name):
+ """Delete a model file."""
+ ctx.obj.delete_file(model_id, file_name)
+
+
+@model.command()
+@click.pass_context
+@click.option("--model-id", "-id", required=True, prompt="Enter model ID")
+def delete(ctx, model_id):
+ """Delete a model."""
+ ctx.obj.delete_model(model_id)
diff --git a/src/commands/user_auth.py b/src/commands/user_auth.py
new file mode 100644
index 0000000..a4652f7
--- /dev/null
+++ b/src/commands/user_auth.py
@@ -0,0 +1,29 @@
+import click
+
+from src.api.api_client import APIClient
+from src.api.auth_api import AuthAPI
+
+endpoint = AuthAPI(APIClient())
+
+
+@click.command()
+@click.option("--email", prompt=True)
+@click.option("--password", prompt=True, hide_input=True)
+def login(email, password):
+ """Login to the application."""
+ result = endpoint.login(email, password)
+ if result["success"]:
+ click.echo(f"Successfully logged in. {result['data']}")
+ else:
+ click.echo(f"Login failed. {result['message']}")
+
+
+@click.command()
+@click.pass_context
+def logout(ctx):
+ """Logout from the application."""
+ result = endpoint.logout()
+ if result:
+ click.echo("Successfully logged out.")
+ else:
+ click.echo("No action taken. You were not logged in.")
diff --git a/src/main.py b/src/main.py
index 92e0c0f..c1a5f78 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,27 +1,40 @@
import click
-from types import ModuleType
-from typing import List
-import src.commands.auth_commands as auth_commands
-import src.commands.metrics_commands as metrics_commands
-import src.commands.machine_commands as machine_commands
-
-@click.group()
-def quack():
- """Quack CLI tool"""
- pass
+import src.commands as commands
+from src.utils.groups.quack_group import QuackGroup
-def add_commands_to_cli(modules: List[ModuleType]) -> None:
- for mod in modules:
- for cmd in dir(mod):
- if isinstance(getattr(mod, cmd), click.core.Command):
- quack.add_command(getattr(mod, cmd))
-
+@click.group(cls=QuackGroup)
+@click.version_option(version="0.1.0")
+@click.pass_context
+def quack(ctx):
+ """A simple CLI tool to help manage your ducks."""
+ pass
-command_modules = [auth_commands, metrics_commands, machine_commands]
-add_commands_to_cli(command_modules)
+cmds = {}
+# register all groups and only standalone commands
+for mod in commands.__all__:
+ groups = [
+ getattr(mod, attr)
+ for attr in dir(mod)
+ if isinstance(getattr(mod, attr), click.core.Group)
+ ]
+ all_cmds = [
+ getattr(mod, attr)
+ for attr in dir(mod)
+ if isinstance(getattr(mod, attr), click.core.Command)
+ ]
+
+ # Register groups and track their commands
+ for group in groups:
+ cmds.update({sub_cmd.name: sub_cmd for sub_cmd in group.commands.values()})
+ quack.add_command(group)
+
+ # Register standalone commands not part of any group
+ for cmd in all_cmds:
+ if cmd.name not in cmds:
+ quack.add_command(cmd)
if __name__ == "__main__":
quack()
diff --git a/src/utils/formatters/__init__.py b/src/utils/formatters/__init__.py
new file mode 100644
index 0000000..ee57a07
--- /dev/null
+++ b/src/utils/formatters/__init__.py
@@ -0,0 +1,3 @@
+"""
+Utility module for formatting and displaying data in the CLI for use in custom groups.
+"""
diff --git a/src/utils/formatters/help_formatter_base.py b/src/utils/formatters/help_formatter_base.py
new file mode 100644
index 0000000..00f6338
--- /dev/null
+++ b/src/utils/formatters/help_formatter_base.py
@@ -0,0 +1,53 @@
+from abc import ABC, abstractmethod
+
+import rich.box as rBox
+from rich.align import Align as rAlign
+from rich.console import Console as rConsole
+from rich.table import Table as rTable
+
+
+class HelpFormatterBase(ABC):
+ item_padding = " " * 4
+
+ @abstractmethod
+ def __init__(self, console: rConsole) -> None:
+ pass
+
+ @abstractmethod
+ def format_header(self, console: rConsole) -> None:
+ pass
+
+ @abstractmethod
+ def format_usage(self, console: rConsole, usage_text: str) -> None:
+ pass
+
+ @abstractmethod
+ def format_options(self, console: rConsole, options: list) -> None:
+ pass
+
+ @abstractmethod
+ def format_commands(self, console: rConsole, commands: dict, title: str) -> None:
+ pass
+
+ @abstractmethod
+ def render(self, console: rConsole) -> None:
+ pass
+
+ def create_main_table(self):
+ self.main_table = rTable(
+ show_header=False,
+ box=rBox.SIMPLE,
+ padding=(0, 0, 0, 0),
+ width=self.width,
+ border_style="cyan",
+ show_edge=True,
+ pad_edge=False,
+ )
+ self.main_table.add_column("Content", style="bright_white")
+
+ def add_section_header(self, title: str):
+ title = f"━━━ {title.upper()}"
+ right_sep = "━" * (self.width - len(title) - 3)
+ self.main_table.add_row(
+ rAlign(f"[bold cyan]{title} {right_sep}[/]"), style="on black"
+ )
diff --git a/src/utils/formatters/quack_formatter.py b/src/utils/formatters/quack_formatter.py
new file mode 100644
index 0000000..e5ddc3a
--- /dev/null
+++ b/src/utils/formatters/quack_formatter.py
@@ -0,0 +1,62 @@
+from pyfiglet import Figlet, FontNotFound
+from rich.align import Align as rAlign
+from rich.console import Console as rConsole
+from rich.panel import Panel as rPanel
+
+from src.utils.formatters.help_formatter_base import HelpFormatterBase
+
+
+class MainframeFormatter(HelpFormatterBase):
+ def __init__(self, console: rConsole, format: str = None) -> None:
+ self.console = console
+ self.format = format if format else "cricket"
+ self.width = max(60, self.console.width - 4)
+ self.main_table = None
+ self.create_main_table()
+
+ def format_header(self, title: str, help: str) -> None:
+ # Keep ASCII art header separate
+ try:
+ fig = Figlet(font=self.format, width=self.width - 1)
+ except FontNotFound:
+ self.console.print(f"Font {self.format} not found. Using default.")
+ fig = Figlet(font="cricket", width=self.width - 1)
+ ascii_art = fig.renderText(title)
+ styled_art = rAlign(f"[bold yellow]{ascii_art}[/]", align="center")
+ styled_help = rPanel(
+ rAlign(f"{help}", align="center"),
+ title="by Duckington Labs",
+ border_style="bright_yellow",
+ )
+ self.main_table.add_row(styled_art)
+ self.main_table.add_row(styled_help)
+
+ def format_usage(self, usage_text: str) -> None:
+ self.add_section_header("USAGE")
+ self.main_table.add_row(f"{self.item_padding}{usage_text}\n")
+
+ def format_options(self, options: list) -> None:
+ self.add_section_header("OPTIONS")
+ for opt, desc in options:
+ self.main_table.add_row(f"{self.item_padding}[magenta]{opt:<15}[/] {desc}")
+ self.main_table.add_row()
+
+ def format_commands(self, commands: dict, title: str) -> None:
+ self.add_section_header(title)
+ if title == "COMMANDS":
+ prefix = "❯"
+ print_newline = True
+ else:
+ prefix = "▶"
+ print_newline = False
+
+ for name, cmd in sorted(commands.items()):
+ self.main_table.add_row(
+ f"{self.item_padding}{prefix} [green]{name:<13}[/] {cmd.help or ''}"
+ )
+ if print_newline:
+ self.main_table.add_row()
+
+ def render(self) -> None:
+ if self.main_table:
+ self.console.print(self.main_table)
diff --git a/src/utils/formatters/subcommand_formatter.py b/src/utils/formatters/subcommand_formatter.py
new file mode 100644
index 0000000..1ba2107
--- /dev/null
+++ b/src/utils/formatters/subcommand_formatter.py
@@ -0,0 +1,45 @@
+from rich.align import Align as rAlign
+from rich.console import Console as rConsole
+from rich.panel import Panel as rPanel
+
+from src.utils.formatters.help_formatter_base import HelpFormatterBase
+
+
+class SubFrameFormatter(HelpFormatterBase):
+ def __init__(self, console: rConsole) -> None:
+ self.console = console
+ self.width = max(60, console.width - 4)
+ self.main_table = None
+ self.create_main_table()
+
+ def format_header(self, title: str, help: str) -> None:
+ styled_help = rPanel(
+ rAlign(f"{help}", align="center"),
+ title=f"quack {title}",
+ border_style="yellow",
+ )
+ self.main_table.add_row(styled_help)
+
+ def format_usage(self, usage_text: str) -> None:
+ self.add_section_header("USAGE")
+ self.main_table.add_row(f"{self.item_padding}{usage_text}")
+ self.main_table.add_row()
+
+ def format_options(self, options: list) -> None:
+ self.add_section_header("OPTIONS")
+ for opt, desc in options:
+ self.main_table.add_row(f"{self.item_padding}[magenta]{opt:<15}[/] {desc}")
+ self.main_table.add_row()
+
+ def format_commands(self, commands: dict, title: str) -> None:
+ self.add_section_header(title)
+ prefix = "❯" if title == "COMMANDS" else "▶"
+
+ for name, cmd in sorted(commands.items()):
+ self.main_table.add_row(
+ f"{self.item_padding}{prefix} [green]{name:<13}[/] {cmd.help or ''}"
+ )
+
+ def render(self) -> None:
+ if self.main_table:
+ self.console.print(self.main_table)
diff --git a/src/utils/groups/__init__.py b/src/utils/groups/__init__.py
new file mode 100644
index 0000000..b497680
--- /dev/null
+++ b/src/utils/groups/__init__.py
@@ -0,0 +1,3 @@
+"""
+Utility module for extending click.Group using custom formatters.
+"""
diff --git a/src/utils/groups/quack_group.py b/src/utils/groups/quack_group.py
new file mode 100644
index 0000000..148afb0
--- /dev/null
+++ b/src/utils/groups/quack_group.py
@@ -0,0 +1,48 @@
+import io
+
+import click
+from rich.console import Console as rConsole
+
+from src.utils.formatters.quack_formatter import MainframeFormatter
+
+
+class QuackGroup(click.Group):
+ def format_help(self, ctx, formatter):
+ sio = io.StringIO()
+ console = rConsole(file=sio, force_terminal=True)
+ help_formatter = MainframeFormatter(console)
+
+ # Format header and initialize table
+ help_formatter.format_header("Quack CLI", self.help)
+
+ help_formatter.format_usage(ctx.get_usage().removeprefix("Usage: "))
+ # Get options from the context's command
+ options = []
+ for param in ctx.command.get_params(ctx):
+ if isinstance(param, click.Option):
+ opt_names = "/".join(param.opts)
+ opt_help = param.help or ""
+ options.append((opt_names, opt_help))
+
+ help_formatter.format_options(options)
+
+ if self.commands:
+ commands = {
+ name: cmd
+ for name, cmd in self.commands.items()
+ if not isinstance(cmd, click.Group)
+ }
+ subcommands = {
+ name: cmd
+ for name, cmd in self.commands.items()
+ if isinstance(cmd, click.Group)
+ }
+
+ if commands:
+ help_formatter.format_commands(commands, "COMMANDS")
+ if subcommands:
+ help_formatter.format_commands(subcommands, "SUBCOMMANDS")
+
+ # Render the final table
+ help_formatter.render()
+ formatter.write(sio.getvalue())
diff --git a/src/utils/groups/subcommand_group.py b/src/utils/groups/subcommand_group.py
new file mode 100644
index 0000000..59b0207
--- /dev/null
+++ b/src/utils/groups/subcommand_group.py
@@ -0,0 +1,48 @@
+import io
+
+import click
+from rich.console import Console as rConsole
+
+from src.utils.formatters.subcommand_formatter import SubFrameFormatter
+
+
+class SubCommandGroup(click.Group):
+ def format_help(self, ctx, formatter):
+ sio = io.StringIO()
+ console = rConsole(file=sio, force_terminal=True)
+ help_formatter = SubFrameFormatter(console)
+ cmd_name = ctx.command.name or ctx.info_name
+
+ help_formatter.format_header(cmd_name, self.help)
+
+ help_formatter.format_usage(ctx.get_usage().removeprefix("Usage: "))
+ # Get options from the context's command
+ options = []
+ for param in ctx.command.get_params(ctx):
+ if isinstance(param, click.Option):
+ opt_names = "/".join(param.opts)
+ opt_help = param.help or ""
+ options.append((opt_names, opt_help))
+
+ help_formatter.format_options(options)
+
+ if self.commands:
+ commands = {
+ name: cmd
+ for name, cmd in self.commands.items()
+ if not isinstance(cmd, click.Group)
+ }
+ subcommands = {
+ name: cmd
+ for name, cmd in self.commands.items()
+ if isinstance(cmd, click.Group)
+ }
+
+ if commands:
+ help_formatter.format_commands(commands, "COMMANDS")
+ if subcommands:
+ help_formatter.format_commands(subcommands, "SUBCOMMANDS")
+
+ # Render the final table
+ help_formatter.render()
+ formatter.write(sio.getvalue())
diff --git a/src/utils/helpers/handle_api_errors.py b/src/utils/helpers/handle_api_errors.py
index 2f16c79..abe8dcc 100644
--- a/src/utils/helpers/handle_api_errors.py
+++ b/src/utils/helpers/handle_api_errors.py
@@ -1,6 +1,6 @@
from typing import Callable
from functools import wraps
-from requests.exceptions import HTTPError
+from requests.exceptions import HTTPError, ConnectionError
def handle_api_errors(func: Callable) -> Callable:
@@ -9,7 +9,20 @@ def wrapper(*args, **kwargs):
try:
return {"success": True, "data": func(*args, **kwargs)}
except HTTPError as http_err:
- return {"success": False, "error": "HTTP error", "message": str(http_err)}
+ return {
+ "success": False,
+ "error": "HTTP error",
+ "message": str(http_err),
+ "status_code": http_err.response.status_code,
+ "response": http_err.response.json(),
+ }
+ except ConnectionError as conn_err:
+ return {
+ "success": False,
+ "error": "Connection refused",
+ "message": str(conn_err),
+ "response": {"detail": "Connection refused"},
+ }
except Exception as err:
return {"success": False, "error": "Unexpected error", "message": str(err)}
diff --git a/test/api/test_client.py b/test/api/test_client.py
index 34c4025..19d8b94 100644
--- a/test/api/test_client.py
+++ b/test/api/test_client.py
@@ -29,6 +29,7 @@ def test_make_request_with_api_key(api_client):
json=None,
data=None,
params=None,
+ files=None,
headers={"X-API-Key": "test_api_key"},
)
assert result == {"data": "test"}
@@ -47,6 +48,7 @@ def test_make_request_with_access_token(api_client):
json={"key": "value"},
data=None,
params=None,
+ files=None,
headers={"Authorization": "Bearer test_access_token"},
)
assert result == {"data": "test"}
@@ -102,6 +104,7 @@ def test_get_method(api_client):
data=None,
params={"param": "value"},
headers={"Custom-Header": "Value", "X-API-Key": ANY},
+ files=None,
)
assert result == {"data": "get_test"}
@@ -116,6 +119,7 @@ def test_post_method(api_client):
data=None,
params=None,
headers={"X-API-Key": ANY},
+ files=None,
)
assert result == {"data": "post_test"}
@@ -130,6 +134,7 @@ def test_put_method(api_client):
data=None,
params=None,
headers={"X-API-Key": ANY},
+ files=None,
)
assert result == {"data": "put_test"}
@@ -144,6 +149,7 @@ def test_delete_method(api_client):
data=None,
params=None,
headers={"X-API-Key": ANY},
+ files=None,
)
assert result == {"data": "delete_test"}
diff --git a/test/api/test_model_file_api.py b/test/api/test_model_file_api.py
new file mode 100644
index 0000000..c6270d1
--- /dev/null
+++ b/test/api/test_model_file_api.py
@@ -0,0 +1,64 @@
+import unittest
+from unittest.mock import patch, mock_open
+from src.api.api_client import APIClient
+from src.api.model_file_api import ModelFileAPI
+
+
+class TestModelFileAPI(unittest.TestCase):
+ def setUp(self):
+ self.api = ModelFileAPI(APIClient())
+
+ @patch("builtins.open", new_callable=mock_open, read_data="test data")
+ @patch.object(APIClient, "post")
+ def test_upload_model_file(self, mock_post, mock_file):
+ mock_post.return_value = {"status": "uploaded"}
+ response = self.api.upload_model_file("test_model", "123", "test.file")
+ self.assertEqual(response, {"success": True, "data": {"status": "uploaded"}})
+ mock_post.assert_called_once()
+ mock_file.assert_called_with("test.file", "rb")
+
+ @patch.object(APIClient, "get")
+ def test_get_model(self, mock_get):
+ mock_get.return_value = {"model_id": "123"}
+ response = self.api.get_model("123")
+ self.assertEqual(response, {"success": True, "data": {"model_id": "123"}})
+ mock_get.assert_called_with("models/123")
+
+ @patch.object(APIClient, "get")
+ def test_get_all_models(self, mock_get):
+ mock_get.return_value = {"models": []}
+ response = self.api.get_all_models()
+ self.assertEqual(response, {"success": True, "data": {"models": []}})
+ mock_get.assert_called_with("models")
+
+ @patch.object(APIClient, "get")
+ def test_read_model_file(self, mock_get):
+ mock_get.return_value = {"content": "file_content"}
+ response = self.api.read_model_file("123", "test.file")
+ self.assertEqual(
+ response, {"success": True, "data": {"content": "file_content"}}
+ )
+ mock_get.assert_called_with("models/123/test.file")
+
+ @patch("builtins.open", new_callable=mock_open, read_data="test data")
+ @patch.object(APIClient, "put")
+ def test_update_model_file(self, mock_put, mock_file):
+ mock_put.return_value = {"status": "updated"}
+ response = self.api.update_model_file("test_model", "123", "test.file")
+ self.assertEqual(response, {"success": True, "data": {"status": "updated"}})
+ mock_put.assert_called_once()
+ mock_file.assert_called_with("test.file", "rb")
+
+ @patch.object(APIClient, "delete")
+ def test_delete_model_file(self, mock_delete):
+ mock_delete.return_value = {"status": "deleted"}
+ response = self.api.delete_model_file("123", "test.file")
+ self.assertEqual(response, {"success": True, "data": {"status": "deleted"}})
+ mock_delete.assert_called_with("models/123/test.file")
+
+ @patch.object(APIClient, "delete")
+ def test_delete_model(self, mock_delete):
+ mock_delete.return_value = {"status": "deleted"}
+ response = self.api.delete_model("123")
+ self.assertEqual(response, {"success": True, "data": {"status": "deleted"}})
+ mock_delete.assert_called_with("models/123")
diff --git a/test/commands/test_auth_commands.py b/test/commands/test_auth_commands.py
deleted file mode 100644
index b645a3a..0000000
--- a/test/commands/test_auth_commands.py
+++ /dev/null
@@ -1,151 +0,0 @@
-import pytest
-from unittest.mock import patch
-from click.testing import CliRunner
-from src.commands.auth_commands import (
- login,
- logout,
- create_api_key,
- list_api_keys,
- delete_api_key,
- set_api_key,
- remove_api_key,
-)
-
-
-@pytest.fixture
-def mock_auth_api():
- with patch("src.commands.auth_commands.AuthAPI") as mock:
- yield mock.return_value
-
-
-@pytest.fixture
-def runner():
- return CliRunner()
-
-
-def test_login_command_success(runner, mock_auth_api):
- mock_auth_api.login.return_value = {"success": True, "data": "Login successful"}
- result = runner.invoke(
- login, ["--email", "test@example.com", "--password", "password"]
- )
- assert result.exit_code == 0
- assert "Successfully logged in" in result.output
- mock_auth_api.login.assert_called_once_with("test@example.com", "password")
-
-
-def test_login_command_failure(runner, mock_auth_api):
- mock_auth_api.login.return_value = {
- "success": False,
- "message": "Invalid credentials",
- }
- result = runner.invoke(
- login, ["--email", "test@example.com", "--password", "wrong_password"]
- )
- assert result.exit_code == 0
- assert "Login failed. Invalid credentials" in result.output
- mock_auth_api.login.assert_called_once_with("test@example.com", "wrong_password")
-
-
-def test_logout_command_success(runner, mock_auth_api):
- mock_auth_api.logout.return_value = True
- result = runner.invoke(logout)
- assert result.exit_code == 0
- assert "Successfully logged out" in result.output
- mock_auth_api.logout.assert_called_once()
-
-
-def test_logout_command_not_logged_in(runner, mock_auth_api):
- mock_auth_api.logout.return_value = False
- result = runner.invoke(logout)
- assert result.exit_code == 0
- assert "No action taken. You were not logged in." in result.output
- mock_auth_api.logout.assert_called_once()
-
-
-def test_create_api_key_command_success(runner, mock_auth_api):
- mock_auth_api.create_api_key.return_value = {"success": True, "data": "new_api_key"}
- result = runner.invoke(create_api_key, ["--validity", "ONE_DAY"])
- assert result.exit_code == 0
- assert "API key created successfully: new_api_key" in result.output
- mock_auth_api.create_api_key.assert_called_once()
-
-
-def test_create_api_key_command_failure(runner, mock_auth_api):
- mock_auth_api.create_api_key.return_value = {
- "success": False,
- "message": "Failed to create API key",
- }
- result = runner.invoke(create_api_key, ["--validity", "ONE_DAY"])
- assert result.exit_code == 0
- assert "Failed to create API key. Failed to create API key" in result.output
- mock_auth_api.create_api_key.assert_called_once()
-
-
-def test_list_api_keys_command_success(runner, mock_auth_api):
- mock_api_keys = [
- {"token": "key1", "created_at": "2023-01-01", "validity": "ONE_DAY"},
- {"token": "key2", "created_at": "2023-01-02", "validity": "ONE_WEEK"},
- ]
- mock_auth_api.list_api_keys.return_value = {"success": True, "data": mock_api_keys}
- result = runner.invoke(list_api_keys)
- assert result.exit_code == 0
- assert "Token: key1" in result.output
- assert "Token: key2" in result.output
- mock_auth_api.list_api_keys.assert_called_once()
-
-
-def test_list_api_keys_command_failure(runner, mock_auth_api):
- mock_auth_api.list_api_keys.return_value = {
- "success": False,
- "message": "Failed to retrieve API keys",
- }
- result = runner.invoke(list_api_keys)
- assert result.exit_code == 0
- assert "Failed to retrieve API keys. Failed to retrieve API keys" in result.output
- mock_auth_api.list_api_keys.assert_called_once()
-
-
-def test_delete_api_key_command_success(runner, mock_auth_api):
- mock_auth_api.delete_api_key.return_value = {
- "success": True,
- "data": "API key deleted",
- }
- result = runner.invoke(delete_api_key, ["--token", "test_token"])
- assert result.exit_code == 0
- assert "API key deleted successfully" in result.output
- mock_auth_api.delete_api_key.assert_called_once_with("test_token")
-
-
-def test_delete_api_key_command_failure(runner, mock_auth_api):
- mock_auth_api.delete_api_key.return_value = {
- "success": False,
- "message": "API key not found",
- }
- result = runner.invoke(delete_api_key, ["--token", "invalid_token"])
- assert result.exit_code == 0
- assert "Failed to delete API key. API key not found" in result.output
- mock_auth_api.delete_api_key.assert_called_once_with("invalid_token")
-
-
-def test_set_api_key_command_success(runner, mock_auth_api):
- mock_auth_api.set_api_key.return_value = True
- result = runner.invoke(set_api_key, ["--api-key", "new_api_key"])
- assert result.exit_code == 0
- assert "API key set successfully" in result.output
- mock_auth_api.set_api_key.assert_called_once_with("new_api_key")
-
-
-def test_remove_api_key_command_success(runner, mock_auth_api):
- mock_auth_api.clear_api_key.return_value = True
- result = runner.invoke(remove_api_key)
- assert result.exit_code == 0
- assert "API key removed successfully" in result.output
- mock_auth_api.clear_api_key.assert_called_once()
-
-
-def test_remove_api_key_command_no_key_set(runner, mock_auth_api):
- mock_auth_api.clear_api_key.return_value = False
- result = runner.invoke(remove_api_key)
- assert result.exit_code == 0
- assert "No API key was set" in result.output
- mock_auth_api.clear_api_key.assert_called_once()
diff --git a/test/commands/test_key.py b/test/commands/test_key.py
new file mode 100644
index 0000000..25211cb
--- /dev/null
+++ b/test/commands/test_key.py
@@ -0,0 +1,146 @@
+import unittest
+from unittest.mock import MagicMock, patch
+from click.testing import CliRunner
+from src.commands.key import key, KeyCommands
+
+
+class TestKeyCommands(unittest.TestCase):
+ def setUp(self):
+ """Set up test fixtures before each test method."""
+ self.runner = CliRunner()
+
+ # Create and setup mock_auth_api
+ self.patcher = patch("src.api.auth_api.AuthAPI")
+ self.mock_auth_api = self.patcher.start()
+ self.mock_instance = self.mock_auth_api.return_value
+
+ # Set up default successful responses
+ self.mock_instance.create_api_key.return_value = {
+ "success": True,
+ "data": "test_key",
+ }
+ self.mock_instance.list_api_keys.return_value = {"success": True, "data": []}
+ self.mock_instance.delete_api_key.return_value = {
+ "success": True,
+ "data": "Deleted",
+ }
+ self.mock_instance.set_api_key.return_value = True
+ self.mock_instance.clear_api_key.return_value = True
+
+ # Setup key_commands
+ self.key_commands = KeyCommands()
+ self.key_commands.client = MagicMock()
+ self.key_commands.endpoint = self.mock_instance
+
+ def tearDown(self):
+ """Clean up after each test method."""
+ self.patcher.stop()
+
+ def test_create_api_key_success(self):
+ self.mock_instance.create_api_key.return_value = {
+ "success": True,
+ "data": "new_api_key",
+ }
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.create_api_key("ONE_DAY")
+ mock_print.assert_called_with("API key created successfully: new_api_key")
+
+ def test_create_api_key_failure(self):
+ self.mock_instance.create_api_key.return_value = {
+ "success": False,
+ "message": "Failed to create key",
+ }
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.create_api_key("ONE_DAY")
+ mock_print.assert_called_with(
+ "Failed to create API key. Failed to create key"
+ )
+
+ def test_list_api_keys_success(self):
+ mock_keys = [
+ {"token": "key1", "created_at": "2023-01-01", "validity": "ONE_DAY"},
+ {"token": "key2", "created_at": "2023-01-02", "validity": "ONE_WEEK"},
+ ]
+ self.mock_instance.list_api_keys.return_value = {
+ "success": True,
+ "data": mock_keys,
+ }
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.list_api_keys()
+ self.assertEqual(mock_print.call_count, 3) # Header + 2 keys
+
+ def test_list_api_keys_empty(self):
+ self.mock_instance.list_api_keys.return_value = {"success": True, "data": []}
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.list_api_keys()
+ mock_print.assert_called_with("API Keys:")
+
+ def test_delete_api_key_success(self):
+ self.mock_instance.delete_api_key.return_value = {
+ "success": True,
+ "data": "Key deleted",
+ }
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.delete_api_key("test_token")
+ mock_print.assert_called_with("API key deleted successfully. Key deleted")
+
+ def test_delete_api_key_failure(self):
+ self.mock_instance.delete_api_key.return_value = {
+ "success": False,
+ "message": "Key not found",
+ }
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.delete_api_key("invalid_token")
+ mock_print.assert_called_with("Failed to delete API key. Key not found")
+
+ def test_set_api_key_success(self):
+ self.mock_instance.set_api_key.return_value = True
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.set_api_key("test_key")
+ mock_print.assert_called_with("API key set successfully.")
+
+ def test_remove_api_key_success(self):
+ self.mock_instance.clear_api_key.return_value = True
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.remove_api_key()
+ mock_print.assert_called_with("API key removed successfully.")
+
+ def test_remove_api_key_no_key(self):
+ self.mock_instance.clear_api_key.return_value = False
+
+ with patch("click.echo") as mock_print:
+ self.key_commands.remove_api_key()
+ mock_print.assert_called_with("No API key was set.")
+
+ @patch("src.commands.key.KeyCommands", autospec=True)
+ def test_cli_create_command(self, key_commands):
+ result = self.runner.invoke(key, ["create", "--validity", "ONE_DAY"])
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.key.KeyCommands", autospec=True)
+ def test_cli_list_command(self, key_commands):
+ result = self.runner.invoke(key, ["list"])
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.key.KeyCommands", autospec=True)
+ def test_cli_delete_command(self, key_commands):
+ result = self.runner.invoke(key, ["delete", "--token", "test_token"])
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.key.KeyCommands", autospec=True)
+ def test_cli_set_command(self, key_commands):
+ result = self.runner.invoke(key, ["set", "--api-key", "test_key"])
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.key.KeyCommands", autospec=True)
+ def test_cli_unset_command(self, key_commands):
+ result = self.runner.invoke(key, ["unset"])
+ self.assertEqual(result.exit_code, 0)
diff --git a/test/commands/test_machine.py b/test/commands/test_machine.py
new file mode 100644
index 0000000..772acd0
--- /dev/null
+++ b/test/commands/test_machine.py
@@ -0,0 +1,151 @@
+import unittest
+from unittest.mock import MagicMock, patch
+from click.testing import CliRunner
+from src.commands.machine import MachineCommands, machine
+
+
+class TestMachineCommands(unittest.TestCase):
+ def setUp(self):
+ """Set up test fixtures before each test method."""
+ self.runner = CliRunner()
+
+ # Create and setup mock_machine_api
+ self.patcher = patch("src.api.machine_api.MachineAPI")
+ self.mock_machine_api = self.patcher.start()
+ self.mock_instance = self.mock_machine_api.return_value
+
+ # Set up default successful responses
+ self.mock_instance.create_machine.return_value = {
+ "success": True,
+ "data": {
+ "machine_id": "test123",
+ "machine_name": "test-machine",
+ "machine_type": "f1.2xlarge",
+ },
+ }
+ self.mock_instance.list_user_machines.return_value = {
+ "success": True,
+ "data": [],
+ }
+ self.mock_instance.start_machine.return_value = {
+ "success": True,
+ "data": {"message": "Machine started successfully"},
+ }
+ self.mock_instance.stop_machine.return_value = {
+ "success": True,
+ "data": {"message": "Machine stopped successfully"},
+ }
+ self.mock_instance.terminate_machine.return_value = {
+ "success": True,
+ "data": {"message": "Machine terminated successfully"},
+ }
+
+ # Setup machine_commands
+ self.machine_commands = MachineCommands()
+ self.machine_commands.client = MagicMock()
+ self.machine_commands.endpoint = self.mock_instance
+
+ def tearDown(self):
+ """Clean up after each test method."""
+ self.patcher.stop()
+
+ def test_create_machine_success(self):
+ with patch("click.echo") as mock_print:
+ self.machine_commands.create("test-machine", "f1.2xlarge")
+ mock_print.assert_called_with(
+ f"Machine created successfully. Details: {self.mock_instance.create_machine.return_value['data']}"
+ )
+
+ def test_create_machine_failure(self):
+ self.mock_instance.create_machine.return_value = {
+ "success": False,
+ "message": "Failed to create machine",
+ }
+
+ with patch("click.echo") as mock_print:
+ self.machine_commands.create("test-machine", "f1.2xlarge")
+ mock_print.assert_called_with(
+ "Failed to create machine. Check machine name and type and try again."
+ )
+
+ def test_list_machines_with_data(self):
+ mock_machines = [
+ {"machine_id": "test123", "machine_name": "test-machine-1"},
+ {"machine_id": "test456", "machine_name": "test-machine-2"},
+ ]
+ self.mock_instance.list_user_machines.return_value = {
+ "success": True,
+ "data": mock_machines,
+ }
+
+ with patch("click.echo") as mock_print:
+ self.machine_commands.list()
+ self.assertEqual(mock_print.call_count, 3) # Header + 2 machines
+
+ def test_list_machines_empty(self):
+ with patch("click.echo") as mock_print:
+ self.machine_commands.list()
+ mock_print.assert_called_with("No machines to list.")
+
+ def test_start_machine_success(self):
+ with patch("click.echo") as mock_print:
+ self.machine_commands.start("test123")
+ mock_print.assert_called_with("Machine started successfully")
+
+ def test_stop_machine_success(self):
+ with patch("click.echo") as mock_print:
+ self.machine_commands.stop("test123")
+ mock_print.assert_called_with("Machine stopped successfully")
+
+ def test_terminate_machine_success(self):
+ with patch("click.echo") as mock_print:
+ self.machine_commands.terminate("test123")
+ mock_print.assert_called_with("Machine terminated successfully")
+
+ def test_get_machine_details_success(self):
+ self.mock_instance.get_machine.return_value = {
+ "success": True,
+ "data": {
+ "machine_id": "test123",
+ "machine_name": "test-machine",
+ "machine_type": "f1.2xlarge",
+ },
+ }
+
+ with patch("click.echo") as mock_print:
+ self.machine_commands.get_details("test123")
+ self.assertEqual(mock_print.call_count, 2)
+
+ # CLI command tests
+ @patch("src.commands.machine.MachineCommands", autospec=True)
+ def test_cli_create_command(self, machine_commands):
+ result = self.runner.invoke(
+ machine,
+ ["create", "-n", "test-machine", "-t", "f1.2xlarge"],
+ )
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.machine.MachineCommands", autospec=True)
+ def test_cli_list_command(self, machine_commands):
+ result = self.runner.invoke(machine, ["list"])
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.machine.MachineCommands", autospec=True)
+ def test_cli_start_command(self, machine_commands):
+ result = self.runner.invoke(machine, ["start", "--machine-id", "test123"])
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.machine.MachineCommands", autospec=True)
+ def test_cli_stop_command(self, machine_commands):
+ result = self.runner.invoke(machine, ["stop", "--machine-id", "test123"])
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.machine.MachineCommands", autospec=True)
+ def test_cli_terminate_command(self, machine_commands):
+ result = self.runner.invoke(machine, ["terminate", "--machine-id", "test123"])
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.machine.MachineCommands", autospec=True)
+ def test_cli_details_command(self, machine_commands):
+ result = self.runner.invoke(machine, ["details", "--machine-id", "test123"])
+ self.assertEqual(result.exit_code, 0)
diff --git a/test/commands/test_machine_commands.py b/test/commands/test_machine_commands.py
deleted file mode 100644
index 0c01a6f..0000000
--- a/test/commands/test_machine_commands.py
+++ /dev/null
@@ -1,217 +0,0 @@
-import unittest
-from unittest.mock import patch
-from click.testing import CliRunner
-
-from src.api.machine_api import MachineAPI
-from src.commands.machine_commands import (
- list_machines,
- create_machine,
- start_machine,
- stop_machine,
- terminate_machine,
- get_machine_details,
-)
-
-
-class TestMachineCommands(unittest.TestCase):
-
- def setUp(self):
- self.runner = CliRunner()
-
- @patch.object(MachineAPI, "list_user_machines")
- def test_list_user_machines_with_no_machines(self, mock_list_user_machines):
- mock_response = {"success": True, "data": []}
- mock_list_user_machines.return_value = mock_response
-
- result = self.runner.invoke(list_machines)
-
- self.assertEqual("No machines to list.\n", result.output)
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "list_user_machines")
- def test_list_user_machines_with_some_machines(self, mock_list_user_machines):
- mock_list = [
- {
- "machine_id": "i-0dfa0b805940358fb",
- "machine_name": "test-cli-machine",
- "machine_type": "f1.2xlarge",
- "machine_status": "running",
- "hourly_price": 0.0116,
- "machine_desc": [
- {"Key": "Name", "Value": "test-cli-machine-create"},
- {"Key": "assigned", "Value": "true"},
- {"Key": "user_id", "Value": "user_1234"},
- ],
- }
- ]
- mock_response = {"success": True, "data": mock_list}
- mock_list_user_machines.return_value = mock_response
-
- result = self.runner.invoke(list_machines)
-
- self.assertIn("Machines:\n" + str(mock_list[0]) + "\n", result.output)
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "list_user_machines")
- def test_list_user_machines_fail(self, mock_list_user_machines):
- mock_response = {"success": False, "data": []}
- mock_list_user_machines.return_value = mock_response
-
- result = self.runner.invoke(list_machines)
-
- self.assertEqual("Failed to retrieve list of machines.\n", result.output)
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "create_machine")
- def test_create_machine_success(self, mock_create_machine):
- mock_machine = {
- "machine_id": "i-0dfa0b805940358fb",
- "machine_name": "test-cli-machine",
- "machine_type": "f1.2xlarge",
- "machine_status": "running",
- "hourly_price": 0.0116,
- "machine_desc": [
- {"Key": "Name", "Value": "test-cli-machine-create"},
- {"Key": "assigned", "Value": "true"},
- {"Key": "user_id", "Value": "user_1234"},
- ],
- }
- mock_response = {"success": True, "data": mock_machine}
- mock_create_machine.return_value = mock_response
-
- result = self.runner.invoke(
- create_machine,
- ["--machine-name", "test-cli-machine", "--machine-type", "f1.2xlarge"],
- )
-
- self.assertIn(
- f"Machine created successfully. Details: {mock_machine}\n", result.output
- )
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "create_machine")
- def test_create_machine_fail(self, mock_create_machine):
- mock_machine = {}
- mock_response = {"success": False, "data": mock_machine}
- mock_create_machine.return_value = mock_response
-
- result = self.runner.invoke(
- create_machine,
- ["--machine-name", "test-cli-machine", "--machine-type", "f1.2xlarge"],
- )
-
- self.assertIn(
- "Failed to create machine. Check machine name and type and try again.\n",
- result.output,
- )
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "start_machine")
- def test_start_machine_success(self, mock_start_machine):
- mock_message = {"message": "Machine test1234 started successfully"}
- mock_response = {"success": True, "data": mock_message}
- mock_start_machine.return_value = mock_response
-
- result = self.runner.invoke(start_machine, ["--machine-id", "test1234"])
-
- self.assertIn("Machine test1234 started successfully\n", result.output)
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "start_machine")
- def test_start_machine_fail(self, mock_start_machine):
- mock_message = {"message": "Failed"}
- mock_response = {"success": False, "data": mock_message}
- mock_start_machine.return_value = mock_response
-
- result = self.runner.invoke(start_machine, ["--machine-id", "test1234"])
-
- self.assertIn(
- "Failed to start machine. Check machine ID and try again.\n", result.output
- )
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "stop_machine")
- def test_stop_machine_success(self, mock_stop_machine):
- mock_message = {"message": "Machine test1234 stopped successfully"}
- mock_response = {"success": True, "data": mock_message}
- mock_stop_machine.return_value = mock_response
-
- result = self.runner.invoke(stop_machine, ["--machine-id", "test1234"])
-
- self.assertIn("Machine test1234 stopped successfully\n", result.output)
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "stop_machine")
- def test_stop_machine_fail(self, mock_stop_machine):
- mock_message = {"message": "Failed"}
- mock_response = {"success": False, "data": mock_message}
- mock_stop_machine.return_value = mock_response
-
- result = self.runner.invoke(stop_machine, ["--machine-id", "test1234"])
-
- self.assertIn(
- "Failed to stop machine. Check machine ID and try again.\n", result.output
- )
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "terminate_machine")
- def test_terminate_machine_success(self, mock_terminate_machine):
- mock_message = {"message": "Machine test1234 terminated successfully"}
- mock_response = {"success": True, "data": mock_message}
- mock_terminate_machine.return_value = mock_response
-
- result = self.runner.invoke(terminate_machine, ["--machine-id", "test1234"])
-
- self.assertIn("Machine test1234 terminated successfully\n", result.output)
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "terminate_machine")
- def test_terminate_machine_fail(self, mock_terminate_machine):
- mock_message = {"message": "Failed"}
- mock_response = {"success": False, "data": mock_message}
- mock_terminate_machine.return_value = mock_response
-
- result = self.runner.invoke(terminate_machine, ["--machine-id", "test1234"])
-
- self.assertIn(
- "Failed to terminate machine. Check machine ID and try again.\n",
- result.output,
- )
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "get_machine")
- def test_get_machine_details_success(self, mock_get_machine):
- mock_machine = {
- "machine_id": "test1234",
- "machine_name": "test-cli-2",
- "machine_type": "f1.2xlarge",
- "machine_status": "running",
- "hourly_price": 0.0116,
- "machine_desc": [
- {"Key": "Name", "Value": "test-cli-2"},
- {"Key": "assigned", "Value": "true"},
- {"Key": "user_id", "Value": "user_1234"},
- ],
- }
- mock_response = {"success": True, "data": mock_machine}
- mock_get_machine.return_value = mock_response
-
- result = self.runner.invoke(get_machine_details, ["--machine-id", "test1234"])
-
- self.assertIn(
- "Machine test1234 details:\n" + str(mock_machine) + "\n", result.output
- )
- self.assertEqual(result.exit_code, 0)
-
- @patch.object(MachineAPI, "get_machine")
- def test_get_machine_details_fail(self, mock_get_machine):
- mock_machine = {"failed"}
- mock_response = {"success": False, "data": mock_machine}
- mock_get_machine.return_value = mock_response
-
- result = self.runner.invoke(get_machine_details, ["--machine-id", "test1234"])
-
- self.assertIn(
- "Failed to Get machine. Check machine ID and try again.\n", result.output
- )
- self.assertEqual(result.exit_code, 0)
diff --git a/test/commands/test_metrics.py b/test/commands/test_metrics.py
new file mode 100644
index 0000000..3fdca40
--- /dev/null
+++ b/test/commands/test_metrics.py
@@ -0,0 +1,14 @@
+import unittest
+from click.testing import CliRunner
+from src.commands.metrics import metrics
+
+
+class TestMachineMetricsCommand(unittest.TestCase):
+ def setUp(self):
+ """Set up test fixtures before each test method."""
+ self.runner = CliRunner()
+
+ def test_cli_machine_metrics(self):
+ result = self.runner.invoke(metrics)
+ assert result.exit_code == 0
+ assert "Function not yet supported." in result.output
diff --git a/test/commands/test_metrics_commands.py b/test/commands/test_metrics_commands.py
deleted file mode 100644
index 610af28..0000000
--- a/test/commands/test_metrics_commands.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import pytest
-from click.testing import CliRunner
-from src.commands.metrics_commands import machine_metrics
-
-@pytest.fixture
-def runner():
- return CliRunner()
-
-def test_machine_metrics(runner):
- result = runner.invoke(machine_metrics)
- assert result.exit_code == 0
- assert "Function not yet supported." in result.output
diff --git a/test/commands/test_model_file.py b/test/commands/test_model_file.py
new file mode 100644
index 0000000..1d5ded1
--- /dev/null
+++ b/test/commands/test_model_file.py
@@ -0,0 +1,145 @@
+import unittest
+from unittest.mock import MagicMock, patch
+from click.testing import CliRunner
+from src.commands.model_file import model, ModelFileCommands
+import traceback
+
+
+class TestModelFileCommands(unittest.TestCase):
+ def setUp(self):
+ """Set up test fixtures before each test method."""
+ self.runner = CliRunner()
+
+ # Create and setup mock_model_api
+ self.patcher = patch("src.api.model_file_api.ModelFileAPI")
+ self.mock_model_api = self.patcher.start()
+ self.mock_instance = self.mock_model_api.return_value
+
+ # Set up default successful responses
+ self.mock_instance.upload_model_file.return_value = {
+ "success": True,
+ "data": {
+ "model_name": "test_model",
+ "model_id": "123",
+ "upload_date": "2023-01-01T12:00:00.000+0000",
+ },
+ }
+ self.mock_instance.get_all_models.return_value = {
+ "success": True,
+ "data": [],
+ "response": {"detail": None},
+ }
+ self.mock_instance.get_model.return_value = {
+ "success": True,
+ "data": {"model_name": "test_model", "model_id": "123", "files": []},
+ }
+ self.mock_instance.read_model_file.return_value = {
+ "success": True,
+ "data": {"content": "test content"},
+ }
+ self.mock_instance.delete_model_file.return_value = {
+ "success": True,
+ "data": None,
+ }
+ self.mock_instance.delete_model.return_value = {"success": True, "data": None}
+
+ # Setup model_commands
+ self.model_commands = ModelFileCommands()
+ self.model_commands.client = MagicMock()
+ self.model_commands.endpoint = self.mock_instance
+
+ def tearDown(self):
+ """Clean up after each test method."""
+ self.patcher.stop()
+
+ def test_upload_model_file_success(self):
+ with patch("click.echo") as mock_print:
+ self.model_commands.upload("test.txt", "test_model")
+ mock_print.assert_any_call("\nAttempting to upload model file...\n")
+ mock_print.assert_any_call("Model file uploaded successfully:")
+
+ def test_upload_model_file_failure(self):
+ self.mock_instance.upload_model_file.return_value = {
+ "success": False,
+ "response": {"detail": "Upload failed"},
+ }
+ with patch("click.echo") as mock_print:
+ self.model_commands.upload("test.txt", "test_model")
+ mock_print.assert_called_with("Failed to upload model file: Upload failed")
+
+ def test_list_models_success(self):
+ mock_models = [
+ {
+ "model_name": "model1",
+ "model_id": "123",
+ "files": [
+ {
+ "file_name": "test.txt",
+ "file_size": 100,
+ "last_modified": "2023-01-01T12:00:00Z",
+ }
+ ],
+ }
+ ]
+ self.mock_instance.get_all_models.return_value = {
+ "success": True,
+ "data": mock_models,
+ }
+ with patch("click.echo") as mock_print:
+ self.model_commands.list()
+ mock_print.assert_any_call("Models:")
+
+ def test_get_model_success(self):
+ with patch("click.echo") as mock_print:
+ self.model_commands.get_model("123")
+ mock_print.assert_any_call("Name: test_model")
+ mock_print.assert_any_call("ID: 123")
+
+ def test_read_file_success(self):
+ with patch("click.echo") as mock_print:
+ self.model_commands.read_file("123", "test.txt")
+ mock_print.assert_any_call("File contents for test.txt:")
+ mock_print.assert_any_call("test content")
+
+ def test_delete_file_success(self):
+ with patch("click.echo") as mock_print:
+ self.model_commands.delete_file("123", "test.txt")
+ mock_print.assert_called_with("Model file test.txt deleted successfully")
+
+ def test_delete_model_success(self):
+ with patch("click.echo") as mock_print:
+ self.model_commands.delete_model("123")
+ mock_print.assert_called_with("Model 123 deleted successfully")
+
+ @patch("src.commands.model_file.ModelFileCommands", autospec=True)
+ def test_cli_upload_command(self, model_commands):
+ with self.runner.isolated_filesystem():
+ with open("test.txt", "w") as f:
+ f.write("test")
+ result = self.runner.invoke(
+ model, ["upload", "-n", "test_model", "-f", "test.txt"]
+ )
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.model_file.ModelFileCommands", autospec=True)
+ def test_cli_list_command(self, model_commands):
+ result = self.runner.invoke(model, ["list"])
+ if result.exit_code != 0:
+ print(f"Exit code: {result.exit_code}")
+ print(f"Output: {result.output}")
+ print(f"Exception: {result.exception}")
+ if result.exc_info:
+ print("Full traceback:")
+ print("".join(traceback.format_exception(*result.exc_info)))
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.model_file.ModelFileCommands", autospec=True)
+ def test_cli_get_command(self, model_commands):
+ result = self.runner.invoke(model, ["get", "--model-id", "123"])
+ print(result.output)
+ self.assertEqual(result.exit_code, 0)
+
+ @patch("src.commands.model_file.ModelFileCommands", autospec=True)
+ def test_cli_delete_command(self, model_commands):
+ result = self.runner.invoke(model, ["delete", "--model-id", "123"])
+ self.assertEqual(result.exit_code, 0)
diff --git a/test/commands/test_user_auth.py b/test/commands/test_user_auth.py
new file mode 100644
index 0000000..1accdae
--- /dev/null
+++ b/test/commands/test_user_auth.py
@@ -0,0 +1,70 @@
+import unittest
+from unittest.mock import patch
+from click.testing import CliRunner
+from src.commands.user_auth import login, logout
+
+
+class TestUserAuth(unittest.TestCase):
+ def setUp(self):
+ """Set up test fixtures before each test method."""
+ self.runner = CliRunner()
+
+ # Create and setup mock_auth_api
+ self.patcher = patch("src.api.auth_api.AuthAPI")
+ self.mock_auth_api = self.patcher.start()
+ self.mock_instance = self.mock_auth_api.return_value
+
+ self.endpoint = patch(
+ "src.commands.user_auth.endpoint", self.mock_instance
+ ).start()
+
+ def tearDown(self):
+ """Clean up after each test method."""
+ self.patcher.stop()
+ self.endpoint.stop()
+
+ def test_login_success(self):
+ """Test successful login."""
+ self.mock_instance.login.return_value = {
+ "success": True,
+ "data": "Login successful",
+ }
+
+ with patch("click.echo") as mock_print:
+ result = self.runner.invoke(
+ login, ["--email", "test@test.com", "--password", "password"]
+ )
+ mock_print.assert_called_with("Successfully logged in. Login successful")
+ self.assertEqual(result.exit_code, 0)
+
+ def test_login_failure(self):
+ """Test failed login."""
+ self.mock_instance.login.return_value = {
+ "success": False,
+ "message": "Invalid credentials",
+ }
+
+ with patch("click.echo") as mock_print:
+ result = self.runner.invoke(
+ login, ["--email", "test@test.com", "--password", "wrong"]
+ )
+ mock_print.assert_called_with("Login failed. Invalid credentials")
+ self.assertEqual(result.exit_code, 0)
+
+ def test_logout_success(self):
+ """Test successful logout."""
+ self.mock_instance.logout.return_value = True
+
+ with patch("click.echo") as mock_print:
+ result = self.runner.invoke(logout)
+ mock_print.assert_called_with("Successfully logged out.")
+ self.assertEqual(result.exit_code, 0)
+
+ def test_logout_not_logged_in(self):
+ """Test logout when not logged in."""
+ self.mock_instance.logout.return_value = False
+
+ with patch("click.echo") as mock_print:
+ result = self.runner.invoke(logout)
+ mock_print.assert_called_with("No action taken. You were not logged in.")
+ self.assertEqual(result.exit_code, 0)