diff --git a/superinvoke/__init__.py b/superinvoke/__init__.py index 574ce58..619bab7 100644 --- a/superinvoke/__init__.py +++ b/superinvoke/__init__.py @@ -4,8 +4,8 @@ import invoke -from .constants import Directories, Platforms, console +from . import utils +from .constants import Paths, Platforms, console from .extensions.task import task from .main import init -from .objects import Tags, Tool, Tools -from .utils import path +from .objects import Env, Envs, Tags, Tool, Tools diff --git a/superinvoke/collections/__init__.py b/superinvoke/collections/__init__.py index 38a8d32..6ddfb01 100644 --- a/superinvoke/collections/__init__.py +++ b/superinvoke/collections/__init__.py @@ -1 +1 @@ -from . import misc, tool +from . import env, misc, tool diff --git a/superinvoke/collections/env.py b/superinvoke/collections/env.py new file mode 100644 index 0000000..51024e4 --- /dev/null +++ b/superinvoke/collections/env.py @@ -0,0 +1,50 @@ +from invoke import task +from rich.table import Table + +from .. import constants, utils + + +@task(default=True) +def list(context): + """List available enviroments.""" + from ..main import __ENVS__ + + table = Table(show_header=True, header_style="bold white") + table.add_column("Name", justify="left") + table.add_column("Tags", style="dim", justify="right") + + for env in __ENVS__.All: + table.add_row( + f"[bold green3]{env.name}[/bold green3]" if env == __ENVS__.Current else env.name, + ", ".join(env.tags), + ) + + constants.console.print("Listing [bold green3]current[/bold green3] and other enviroments:\n") + constants.console.print(table) + + +@task( + help={ + "enviroment": "Environment name to switch to. Example: dev", + } +) +def switch(context, enviroment): + """Switch current environment.""" + from ..main import __ENVS__ + + new_env = __ENVS__.ByName(enviroment) + old_env = __ENVS__.Current + + if not new_env: + context.fail(f"{enviroment} is not a valid enviroment") + + if new_env == old_env: + context.info(f"{enviroment} is already the current enviroment") + return + + context.create(utils.path(constants.Paths.ENV), data=[new_env], dir=False) + + if new_env != __ENVS__.Current: + context.fail(f"Cannot switch to enviroment {enviroment}") + + context.print(f"Switched to enviroment [green3]{new_env}[/green3] from {old_env}") diff --git a/superinvoke/collections/tool.py b/superinvoke/collections/tool.py index 27cabad..2e3bdf9 100644 --- a/superinvoke/collections/tool.py +++ b/superinvoke/collections/tool.py @@ -134,7 +134,7 @@ def install(context, include, exclude="", yes=False): with tempfile.TemporaryDirectory() as TMP: TMP = utils.path(TMP) - context.create(utils.path(constants.Directories.TOOLS), dir=True) + context.create(utils.path(constants.Paths.TOOLS), dir=True) for tool in tools: if not context.has(tool, version=tool.version): diff --git a/superinvoke/constants.py b/superinvoke/constants.py index 7139645..27dfcc5 100644 --- a/superinvoke/constants.py +++ b/superinvoke/constants.py @@ -6,6 +6,7 @@ # Different OS Platforms. +# TODO: Add architechtures. class Platforms(utils.StrEnum): LINUX = "linux" WINDOWS = "win32" @@ -16,13 +17,17 @@ def CURRENT(cls): return Platforms(sys.platform) -# Different superinvoke directories. -class Directories(utils.StrEnum): +# Different superinvoke paths. +class Paths(utils.StrEnum): CACHE = ".superinvoke_cache" @utils.classproperty def TOOLS(cls): - return f"{Directories.CACHE}/tools" + return f"{Paths.CACHE}/tools" + + @utils.classproperty + def ENV(cls): + return f"{Paths.CACHE}/env" # Global console instance. diff --git a/superinvoke/extensions/context.py b/superinvoke/extensions/context.py index b9446ef..f33cd32 100644 --- a/superinvoke/extensions/context.py +++ b/superinvoke/extensions/context.py @@ -1,12 +1,9 @@ -import os -import shutil import sys -from typing import List, Optional +from typing import List, Literal, Optional -from download import download as fetch from invoke.context import Context -from .. import constants +from .. import constants, utils # Writes to stdout and flushes. @@ -97,40 +94,47 @@ def changes(context: Context, scope: int = 1) -> List[str]: # FILE DIR # - copy # - move X X -# - create X +# - create X X # - remove -# - read +# - read X - # - write +# - exists X X # - extract X X # - download X X # Creates a file or a directory in the specified path. def create(context: Context, path: str, data: List[str] = [""], dir: bool = False) -> None: - if dir: - os.makedirs(str(path), exist_ok=True) + utils.create(path, data, dir=dir) + + +# Reads a file in the specified path. +def read(context: Context, path: str) -> List[str]: + return utils.read(path) + + +# Checks if the specified path exists and whether it is a file or a directory. +def exists(context: Context, path: str) -> Optional[Literal["file", "dir"]]: + return utils.exists(path) # Moves a file or a directory to the specified path. def move(context: Context, source_path: str, dest_path: str) -> None: - shutil.move(str(source_path), str(dest_path)) + utils.move(source_path, dest_path) # Removes a file or a directory in the specified path. def remove(context: Context, path: str, dir: bool = False) -> None: - if dir: - shutil.rmtree(str(path)) - else: - os.remove(str(path)) + utils.remove(path, dir=dir) # Extracts a zip, tar, gztar, bztar, or xztar file in the specified path. def extract(context: Context, source_path: str, dest_path: str) -> None: - shutil.unpack_archive(str(source_path), str(dest_path)) + utils.extract(source_path, dest_path) # Downloads a file to the specified path. def download(context: Context, url: str, path: str) -> None: - fetch(str(url), str(path), progressbar=False, replace=True, verbose=False) + utils.download(url, path) # Extends Pyinvoke's Context methods. @@ -148,6 +152,8 @@ def init() -> None: Context.branch = branch Context.changes = changes Context.create = create + Context.read = read + Context.exists = exists Context.move = move Context.remove = remove Context.extract = extract diff --git a/superinvoke/main.py b/superinvoke/main.py index 984bec3..4de8e91 100644 --- a/superinvoke/main.py +++ b/superinvoke/main.py @@ -1,4 +1,5 @@ import os +from typing import Optional from invoke import Collection @@ -7,9 +8,14 @@ # Superinvoke root collection initialization. -def init(tools: objects.Tools) -> Collection: - global __TOOLS__ - __TOOLS__ = tools +def init(tools: Optional[objects.Tools] = None, envs: Optional[objects.Envs] = None) -> Collection: + if tools: + global __TOOLS__ + __TOOLS__ = tools + + if envs: + global __ENVS__ + __ENVS__ = envs context.init() @@ -23,12 +29,20 @@ def init(tools: objects.Tools) -> Collection: }) root.add_task(collections.misc.help) - # Tool collection - tool = Collection() - tool.add_task(collections.tool.install) - tool.add_task(collections.tool.list) - tool.add_task(collections.tool.remove) - tool.add_task(collections.tool.run) - root.add_collection(tool, name="tool") + if tools: + # Tool collection + tool = Collection() + tool.add_task(collections.tool.install) + tool.add_task(collections.tool.list) + tool.add_task(collections.tool.remove) + tool.add_task(collections.tool.run) + root.add_collection(tool, name="tool") + + if envs: + # Environment collection + env = Collection() + env.add_task(collections.env.list) + env.add_task(collections.env.switch) + root.add_collection(env, name="env") return root diff --git a/superinvoke/objects/__init__.py b/superinvoke/objects/__init__.py index cd3ab0c..d684761 100644 --- a/superinvoke/objects/__init__.py +++ b/superinvoke/objects/__init__.py @@ -1 +1,3 @@ -from .tool import Tags, Tool, Tools +from .common import Tags +from .env import Env, Envs +from .tool import Tool, Tools diff --git a/superinvoke/objects/common.py b/superinvoke/objects/common.py new file mode 100644 index 0000000..5505e01 --- /dev/null +++ b/superinvoke/objects/common.py @@ -0,0 +1,6 @@ +from .. import utils + + +# Represents the list of available tags. +class Tags(utils.StrEnum): + pass diff --git a/superinvoke/objects/env.py b/superinvoke/objects/env.py new file mode 100644 index 0000000..56f49d6 --- /dev/null +++ b/superinvoke/objects/env.py @@ -0,0 +1,68 @@ +from typing import Any, Callable, List, Optional + +from .. import constants, utils +from .common import Tags + + +# Represents an environment. +class Env: + name: str + tags: List[Tags] + + def __init__(self, name: str, tags: List[Tags]): + self.name = name + self.tags = tags + + def __str__(self) -> str: + return self.name + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Env) + and self.name == other.name + and self.tags == other.tags + ) + + def __hash__(self) -> int: + return hash((self.name, tuple(self.tags))) + + +# Represents the list of available environments. +class Envs: + Default: Optional[Callable[[Any], Env]] = None + + @utils.classproperty + def All(cls) -> List[Env]: + all = [] + + for env in dir(cls): + if env in ["All", "Default", "Current"]: + continue + + env = getattr(cls, env) + if isinstance(env, Env): + all.append(env) + + return all + + @utils.classproperty + def Current(cls) -> Optional[Env]: + if utils.exists(utils.path(constants.Paths.ENV)) == "file": + return cls.ByName(utils.read(utils.path(constants.Paths.ENV))[0]) + + if hasattr(cls, "Default") and cls.Default is not None: + return cls.Default(cls) + + return None + + @classmethod + def ByTag(cls, tag) -> List[Env]: + return [env for env in cls.All if tag in env.tags] + + @classmethod + def ByName(cls, name) -> Optional[Env]: + for env in cls.All: + if name == env.name: + return env + + return None diff --git a/superinvoke/objects/tool.py b/superinvoke/objects/tool.py index ce58b13..d5565b4 100644 --- a/superinvoke/objects/tool.py +++ b/superinvoke/objects/tool.py @@ -1,11 +1,7 @@ from typing import List, Optional from .. import constants, utils - - -# Represents a tool tag. -class Tags(utils.StrEnum): - pass +from .common import Tags # Represents an executable tool. @@ -20,7 +16,7 @@ def __init__(self, name: str, version: Optional[str], tags: List[Tags], links: d self.name = name self.version = version self.tags = tags - self.path = utils.path(f"{constants.Directories.TOOLS}/{self.name}") + self.path = utils.path(f"{constants.Paths.TOOLS}/{self.name}") self.links = links def __str__(self) -> str: @@ -31,7 +27,11 @@ def link(self) -> tuple: return self.links.get(constants.Platforms.CURRENT, constants.Platforms.LINUX) def __eq__(self, other: object) -> bool: - return self.name == other.name and self.version == other.version + return ( + isinstance(other, Tool) + and self.name == other.name + and self.version == other.version + ) def __hash__(self) -> int: return hash((self.name, self.version)) diff --git a/superinvoke/utils.py b/superinvoke/utils.py index 152d274..7450a71 100644 --- a/superinvoke/utils.py +++ b/superinvoke/utils.py @@ -1,6 +1,10 @@ import os +import shutil from enum import Enum from pathlib import Path +from typing import List, Literal, Optional + +from download import download as fetch # String enumerator. @@ -26,3 +30,54 @@ def path(path: str) -> str: def next_arg(string: str) -> str: lpos = string.find(" ") return (string[:lpos], string[lpos + 1 :]) if lpos != -1 else (string, "") + + +# Creates a file or a directory in the specified path (overwritting). +def create(path: str, data: List[str] = [""], dir: bool = False) -> None: + if dir: + os.makedirs(str(path), exist_ok=True) + else: + data = [str(line) + "\n" for line in data] + with open(str(path), "w") as f: + f.writelines(data) + + +# Reads a file in the specified path. +def read(path: str) -> List[str]: + with open(str(path), "r") as f: + return f.read().splitlines() + + +# Checks if the specified path exists and whether it is a file or a directory. +def exists(path: str) -> Optional[Literal["file", "dir"]]: + if not os.path.exists(str(path)): + return None + elif os.path.isdir(str(path)): + return "dir" + elif os.path.isfile(str(path)): + return "file" + else: + return None + + +# Moves a file or a directory to the specified path. +def move(source_path: str, dest_path: str) -> None: + shutil.move(str(source_path), str(dest_path)) + + +# Removes a file or a directory in the specified path. +def remove(path: str, dir: bool = False) -> None: + if dir: + shutil.rmtree(str(path)) + else: + os.remove(str(path)) + + +# Extracts a zip, tar, gztar, bztar, or xztar file in the specified path. +def extract(source_path: str, dest_path: str) -> None: + shutil.unpack_archive(str(source_path), str(dest_path)) + + +# Downloads a file to the specified path. +def download(url: str, path: str) -> None: + fetch(str(url), str(path), progressbar=False, replace=True, verbose=False)