Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

31 bug fix workspace cli command #40

Merged
merged 3 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A workspace is,

- Project-Specific: Each project can have their own workspace, while sharing the same installation.
- YAML-Based: Workspaces are defined in YAML files, which can be version controlled.
- Stored in the client/user's home directory under the path `~/.workflow/workspaces/`
- Stored in the client/user's home directory under the path `~/.config/workflow`

## How do I activate a workspace?

Expand All @@ -32,13 +32,13 @@ To remove an active workspace,
workflow workspace rm
```

This will only remove the active workspace, i.e. `~/.workflow/workspaces/active.yml`.
This will only remove the active workspace, i.e. `~/.config/workflow/workspace.yml`.

!!! Important

Running workflow without a workspace set will result in an runtime error.

In order to purge all workspaces, from `~/.workflow/workspaces/` run:
In order to purge all workspaces, from `~/.config/workflow/` run:

```bash
workflow workspace purge
Expand Down
12 changes: 5 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
"""pytest configuration file."""

import pytest
from click.testing import CliRunner

from workflow.cli.main import cli as workflow


def pytest_configure(config):
"""Initailize pytest configuration.

Allows plugins and conftest files to perform initial configuration.
This hook is called for every plugin and initial conftest
file after command line options have been parsed.
"""
@pytest.fixture(autouse=True, scope="function")
def set_testing_workspace():
"""Initailize testing workspace."""
runner = CliRunner()
runner.invoke(workflow, ["workspace", "set", "development"])
return True
35 changes: 35 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Test the workspace CLI commands."""

import os

from click.testing import CliRunner

from workflow import CONFIG_PATH, DEFAULT_WORKSPACE_PATH
from workflow.cli.workspace import ls, set


class TestWorkspaceCLI:
def test_workspace_ls(self):
runner = CliRunner()
result = runner.invoke(ls)
assert result.exit_code == 0
assert "From Workflow Python Module" in result.output
assert "development" in result.output

def test_workspace_set(self):
runner = CliRunner()
result = runner.invoke(set, ["development"])
assert result.exit_code == 0
assert "Locating workspace development" in result.output
assert "Workspace development set to active" in result.output
# ? Check the default folder only contains the active workspace file
entries = os.listdir(CONFIG_PATH)
files = [
CONFIG_PATH / entry
for entry in entries
if os.path.isfile(os.path.join(CONFIG_PATH, entry))
]
files = [f.as_posix() for f in files]
assert files == [DEFAULT_WORKSPACE_PATH.as_posix()]
# ? Re set workspace for other tests
result = runner.invoke(set, ["development"])
15 changes: 14 additions & 1 deletion tests/test_http_context.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
"""Test the HTTPContext object."""

import pytest
from click.testing import CliRunner
from pydantic import ValidationError

from workflow.cli.workspace import set, unset
from workflow.http.context import HTTPContext


class TestHTTPContext:
def test_can_be_instantiated(self):
"""Test that the HTTPContext object can be instantiated."""
HTTPContext()
http = HTTPContext()
assert http

def test_cannot_be_instantiated_without_workspace(self):
"""Test that the HTTPContext object cannot be instantiated without workspace."""
runner = CliRunner()
# ? Set Workspace
runner.invoke(unset)
with pytest.raises(ValidationError):
HTTPContext()
runner.invoke(set, ["development"])

@pytest.mark.skip
def test_clients_connect_to_base_url(self):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from workflow.http.context import HTTPContext


def test_work_pass_token_to_client(monkeypatch):
def test_work_pass_token_to_client(monkeypatch, set_testing_workspace):
"""Test that the Client objects can obtain token from Work object."""
test_token = "ghp_1234567890abcdefg"
monkeypatch.setenv("WORKFLOW_HTTP_TOKEN", test_token)
Expand Down
11 changes: 11 additions & 0 deletions tests/test_work.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Test the work object."""

import pytest
from click.testing import CliRunner
from pydantic import ValidationError

from workflow.cli.workspace import set, unset
from workflow.definitions.work import Work


Expand All @@ -24,6 +26,15 @@ def test_bad_pipeline():
Work(pipeline="", site="local", user="test")


def test_worskpace_unset():
"""Test that the work object can't be instantiated without a setted workspace."""
runner = CliRunner()
runner.invoke(unset)
with pytest.raises(ValidationError):
Work(pipeline="", site="local", user="test")
runner.invoke(set, ["development"])


def test_pipeline_reformat():
"""Test that the work object can't be instantiated with empty pipeline."""
with pytest.raises(ValidationError):
Expand Down
9 changes: 7 additions & 2 deletions workflow/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""Top-level imports for Tasks API."""

from pathlib import Path
from warnings import filterwarnings

# Ignore UserWarnings from pydantic_settings module
# These usually come when no docker secrets are found
filterwarnings("ignore", category=UserWarning, module="pydantic_settings")

# Root path to the Workflow Module
MODULE_PATH: Path = Path(__file__).absolute().parent.parent
# Path to local configurations
CONFIG_PATH: Path = Path.home() / ".workflow"
CONFIG_PATH: Path = Path.home() / ".config" / "workflow"
# Active Workspace Path
DEFAULT_WORKSPACE_PATH: Path = CONFIG_PATH / "workspaces" / "active.yml"
DEFAULT_WORKSPACE_PATH: Path = CONFIG_PATH / "workspace.yml"
# Workflow Client Version
__version__ = "0.3.0" # {x-release-please-version}
16 changes: 5 additions & 11 deletions workflow/cli/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,15 @@ def set(workspace: str):

name: str = config["workspace"]
localspaces.mkdir(parents=True, exist_ok=True)
# Copy the workspace config to ~/.workflow/workspaces/<name>.yml
configpath = localspaces / f"{name}.yml"
activepath = localspaces / "active.yml"
# Write config to configpath, even if it already exists.
with open(configpath, "w") as filename:
dump(config, filename)
console.print(f"Copied {name} to {configpath}.", style="bold green")
activepath = localspaces / "workspace.yml"
# Write config to activepath, even if it already exists.
with open(activepath, "w") as filename:
dump(config, filename)
console.print(f"Workspace {name} set to active.", style="bold green")


@workspace.command("read", help="Read workspace config.")
@click.argument("workspace", type=str, required=True, nargs=1, default="active")
@click.argument("workspace", type=str, required=True, nargs=1, default="workspace")
def read(workspace: str):
"""Read the active workspace.

Expand All @@ -120,7 +114,7 @@ def read(workspace: str):
console.print(config, style="green")
return
else:
console.print(f"Workspace {workspace} not found.", style="bold red")
console.print("No workspace found.", style="italic bold red")
return


Expand All @@ -130,14 +124,14 @@ def unset():
# Set the default console style.
console.print("Removing the active workspace.", style="italic red")
# If the workspace already exists, warn the user.
(localspaces / "active.yml").unlink(missing_ok=True)
(localspaces / "workspace.yml").unlink(missing_ok=True)
console.print("Workspace Removed.", style="bold red")


@workspace.command("purge", help="Purge all local workspaces.")
def purge():
"""Purge all local workspaces."""
# Remove all files from ~/.workflow/workspaces/
# Remove all files from ~/.config/workflow/
console.print("Purging all local workspaces", style="italic red")
for workspace in localspaces.glob("*.y*ml"):
console.print(f"Removing {workspace}", style="italic red")
Expand Down
4 changes: 2 additions & 2 deletions workflow/definitions/work.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class Work(BaseSettings):
notify (Notify): Notification configuration for the work.

workspace (FilePath): Path to the active workspace configuration.
Defaults to `~/.workflow/workspaces/active.yml`. (Excluded from payload)
Defaults to `~/.config/workflow/workspace.yml`. (Excluded from payload)
token (SecretStr): Workflow Access Token. (Excluded from payload)
http (HTTPContext): HTTP Context for backend connections. (Excluded from payload)

Expand Down Expand Up @@ -130,7 +130,7 @@ class Work(BaseSettings):
default=DEFAULT_WORKSPACE_PATH,
validate_default=True,
description="Default workspace configuration filepath.",
examples=["/home/user/.workflow/active.yml"],
examples=["/home/user/.config/workflow/workspace.yml"],
exclude=True,
)
token: SecretStr | None = Field(
Expand Down
34 changes: 31 additions & 3 deletions workflow/http/context.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
"""HTTP client for interacting with the Workflow Servers."""

import os
from typing import Any, Dict, Optional

from pydantic import AliasChoices, Field, FilePath, SecretStr, model_validator
from pydantic import (
AliasChoices,
Field,
FilePath,
SecretStr,
field_validator,
model_validator,
)
from pydantic_settings import BaseSettings, SettingsConfigDict

from workflow import DEFAULT_WORKSPACE_PATH
Expand Down Expand Up @@ -42,8 +50,8 @@ class HTTPContext(BaseSettings):
workspace: FilePath = Field(
default=DEFAULT_WORKSPACE_PATH,
frozen=True,
description="Path to the active workspace configuration.",
examples=["/path/to/workspace/config.yaml"],
description="Path to the workspace configuration.",
examples=["/home/user/.config/workflow/workspace.yaml"],
)
timeout: float = Field(
default=15.0,
Expand Down Expand Up @@ -91,6 +99,26 @@ class HTTPContext(BaseSettings):
exclude=True,
)

@field_validator("workspace", mode="before")
@classmethod
def check_workspace_is_set(cls, value: str):
"""Check that workspace field has a valid filepath.

Parameters
----------
value : str
FilePath str value.

Raises
------
ValueError
If path is not a valid file.
"""
if not os.path.isfile(value):
logger.error("No workspace set.")
raise ValueError("No workspace set.")
return value

@model_validator(mode="after")
def create_clients(self) -> "HTTPContext":
"""Create the HTTP Clients for the Workflow Servers.
Expand Down
Loading