diff --git a/cpl.workspace.json b/cpl.workspace.json new file mode 100644 index 00000000..885653e9 --- /dev/null +++ b/cpl.workspace.json @@ -0,0 +1,11 @@ +{ + "name": "cpl", + "projects": [ + "src/cpl-core/cpl.project.json", + "src/cpl-cli/cpl.project.json" + ], + "defaultProject": "cpl-cli", + "scripts": { + "format": "black src" + } +} \ No newline at end of file diff --git a/src/cpl-cli/cpl.project.json b/src/cpl-cli/cpl.project.json new file mode 100644 index 00000000..36b88951 --- /dev/null +++ b/src/cpl-cli/cpl.project.json @@ -0,0 +1,21 @@ +{ + "name": "cpl-cli", + "version": "0.1.0", + "type": "console", + "license": "MIT", + "author": "Sven Heidemann", + "description": "CLI for the CPL library", + "homepage": "", + "keywords": [], + "dependencies": { + "click": "~8.3.0" + }, + "devDependencies": { + "black": "25.1.0" + }, + "references": [ + "../cpl/cpl.project.json" + ], + "main": "cpl/cli/main.py", + "directory": "cpl/cli" +} \ No newline at end of file diff --git a/src/cpl-cli/cpl/cli/__init__.py b/src/cpl-cli/cpl/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-cli/cpl/cli/cli.py b/src/cpl-cli/cpl/cli/cli.py new file mode 100644 index 00000000..66d633fc --- /dev/null +++ b/src/cpl-cli/cpl/cli/cli.py @@ -0,0 +1,33 @@ +import click + + +class AliasedGroup(click.Group): + def command(self, *args, **kwargs): + aliases = kwargs.pop("aliases", []) + + def decorator(f): + cmd = super(AliasedGroup, self).command(*args, **kwargs)(f) + + for alias in aliases: + self.add_command(cmd, alias) + return cmd + + return decorator + + def format_commands(self, ctx, formatter): + commands = [] + seen = set() + for name, cmd in self.commands.items(): + if cmd in seen: + continue + seen.add(cmd) + aliases = [a for a, c in self.commands.items() if c is cmd and a != name] + alias_text = f" (aliases: {', '.join(aliases)})" if aliases else "" + commands.append((name, f"{cmd.short_help or ''}{alias_text}")) + + with formatter.section("Commands"): + formatter.write_dl(commands) + + +@click.group(cls=AliasedGroup) +def cli(): ... diff --git a/src/cpl-cli/cpl/cli/command/__init__.py b/src/cpl-cli/cpl/cli/command/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-cli/cpl/cli/command/init.py b/src/cpl-cli/cpl/cli/command/init.py new file mode 100644 index 00000000..ff4bc7cd --- /dev/null +++ b/src/cpl-cli/cpl/cli/command/init.py @@ -0,0 +1,86 @@ +from pathlib import Path + +import click + +from cpl.cli.const import PROJECT_TYPES, PROJECT_TYPES_SHORT +from cpl.cli.model.project import Project +from cpl.cli.model.workspace import Workspace +from cpl.cli.utils.prompt import SmartChoice +from cpl.core.console import Console + + +@click.command("init") +@click.argument("target", required=False) +@click.argument("name", required=False) +def init(target: str, name: str): + if target is None: + Console.write_line("CPL Init Wizard") + target = click.prompt( + "What do you want to initialize?", + type=SmartChoice(["workspace"] + PROJECT_TYPES, {"workspace": "ws"}), + show_choices=True, + ) + + if target in PROJECT_TYPES_SHORT: + target = [pt for pt in PROJECT_TYPES if pt.startswith(target)][0] + + if target in ["workspace", "ws"]: + _init_workspace(name or click.prompt("Workspace name", default="my-workspace")) + elif target in PROJECT_TYPES: + _init_project(name or click.prompt("Project name", default=f"my-{target}"), target) + else: + Console.error(f"Unknown target '{target}'") + raise SystemExit(1) + + +def _init_workspace(name: str): + path = Path("cpl.workspace.json") + + if path.exists(): + Console.write_line("workspace.json already exists.") + return + + data = { + "name": name, + "projects": [], + "defaultProject": None, + "scripts": {}, + } + workspace = Workspace.new("./", name) + workspace.save() + + Console.write_line(f"Created workspace '{name}'") + + +def _init_project(name: str, project_type: str): + project = Project.new("./", name, project_type) + + path = Path("cpl.project.json") + if path.exists(): + Console.write_line("cpl.project.json already exists.") + return + + if not Path("src").exists(): + project.directory = click.prompt( + "Project directory", type=click.Path(exists=True, file_okay=False), default="src" + ) + + if project_type in ["console", "web", "service"]: + project.main = click.prompt( + "Main executable", type=click.Path(exists=True, dir_okay=False), default="src/main.py" + ) + + project.save() + + workspace_file = Path("../cpl.workspace.json") + if workspace_file.exists(): + workspace = Workspace.from_file(workspace_file) + + rel_path = str(Path.cwd().relative_to(workspace_file.parent)) + if rel_path not in workspace.projects: + workspace.projects.append(rel_path) + workspace.save() + + Console.write_line(f"Registered '{name}' in workspace.json") + + Console.write_line(f"Created {project_type} project '{name}'") diff --git a/src/cpl-cli/cpl/cli/command/install.py b/src/cpl-cli/cpl/cli/command/install.py new file mode 100644 index 00000000..807b2c4f --- /dev/null +++ b/src/cpl-cli/cpl/cli/command/install.py @@ -0,0 +1,41 @@ +import subprocess +from pathlib import Path + +import click + +from cpl.cli.cli import cli +from cpl.cli.utils.pip import Pip +from cpl.cli.utils.structure import resolve_project +from cpl.core.console import Console + + +@cli.command("install", aliases=["i"]) +@click.option( + "--path", + "project_path", + type=click.Path(file_okay=False, exists=False), + default=".", + help="Path to project directory", +) +@click.option("--name", "project_name", help="Project name (lookup via workspace)") +@click.option("--dev", is_flag=True, help="Include dev dependencies") +@click.option("--verbose", is_flag=True, help="Enable verbose output") +def install(project_path: str, project_name: str, dev: bool, verbose: bool): + project = resolve_project(Path(project_path), project_name) + deps: dict = project.dependencies + if dev: + deps.update(project.dev_dependencies) + + if not deps: + Console.error("No dependencies to install.") + return + + Console.write_line(f"Installing dependencies for '{project.name}':") + + for name, version in deps.items(): + dep = Pip.normalize_dep(name, version) + Console.write_line(f" -> {dep}") + try: + Pip.command(project_path, "install", dep, verbose=verbose) + except subprocess.CalledProcessError as e: + Console.error(f"Failed to install {dep}: exit code {e.returncode}") diff --git a/src/cpl-cli/cpl/cli/command/uninstall.py b/src/cpl-cli/cpl/cli/command/uninstall.py new file mode 100644 index 00000000..a2a7bfe1 --- /dev/null +++ b/src/cpl-cli/cpl/cli/command/uninstall.py @@ -0,0 +1,39 @@ +import subprocess +from pathlib import Path + +import click + +from cpl.cli.cli import cli +from cpl.cli.utils.pip import Pip +from cpl.cli.utils.structure import resolve_project +from cpl.core.console import Console + + +@cli.command("uninstall", aliases=["ui"]) +@click.argument("package", required=False) +@click.option( + "--path", + "project_path", + type=click.Path(file_okay=False, exists=False), + default=".", + help="Path to project directory", +) +@click.option("--name", "project_name", help="Project name (lookup via workspace)") +@click.option("--dev", is_flag=True, help="Include dev dependencies") +@click.option("--verbose", is_flag=True, help="Enable verbose output") +def uninstall(package: str, project_path: str, project_name: str, dev: bool, verbose: bool): + if package is None: + package = Console.read("Package name to uninstall: ").strip() + + project = resolve_project(Path(project_path), project_name) + deps = project.dependencies if not dev else project.dev_dependencies + + try: + Pip.command(project_path, "install", package, verbose=verbose) + except subprocess.CalledProcessError as e: + Console.error(f"Failed to uninstall {package}: exit code {e.returncode}") + + if package in deps: + del deps[package] + project.save() + Console.write_line(f"Removed {package} from project dependencies.") diff --git a/src/cpl-cli/cpl/cli/const.py b/src/cpl-cli/cpl/cli/const.py new file mode 100644 index 00000000..a5810f03 --- /dev/null +++ b/src/cpl-cli/cpl/cli/const.py @@ -0,0 +1,2 @@ +PROJECT_TYPES = ["console", "web", "library", "service"] +PROJECT_TYPES_SHORT = [x[0] for x in PROJECT_TYPES] diff --git a/src/cpl-cli/cpl/cli/main.py b/src/cpl-cli/cpl/cli/main.py new file mode 100644 index 00000000..3d8f2bea --- /dev/null +++ b/src/cpl-cli/cpl/cli/main.py @@ -0,0 +1,66 @@ +import os +from pathlib import Path + +from cpl.cli.cli import cli +from cpl.cli.command.init import init +from cpl.cli.command.install import install +from cpl.cli.command.uninstall import uninstall +from cpl.cli.model.workspace import Workspace +from cpl.cli.utils.custom_command import script_command +from cpl.core.configuration import Configuration + + +def _load_workspace(path: str) -> Workspace | None: + path = Path(path) + if not path.exists() or path.is_dir(): + return None + + return Workspace.from_file(path) + + +def _load_scripts(): + for p in [ + "./cpl.workspace.json", + "../cpl.workspace.json", + "../../cpl.workspace.json", + ]: + ws = _load_workspace(p) + if ws is None: + continue + + Configuration.set("workspace_path", os.path.abspath(p)) + return ws.scripts + + return {} + + +def prepare(): + scripts = _load_scripts() + for name, command in scripts.items(): + script_command(cli, name, command) + + +def configure(): + cli.add_command(init) + cli.add_command(install) + cli.add_command(uninstall) + + +def main(): + prepare() + configure() + cli() + + +if __name__ == "__main__": + main() + +# (( +# ( `) +# ; / , +# / \/ +# / | +# / ~/ +# / ) ) ~ edraft +# ___// | / +# `--' \_~-, diff --git a/src/cpl-cli/cpl/cli/model/__init__.py b/src/cpl-cli/cpl/cli/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-cli/cpl/cli/model/cpl_structure_model.py b/src/cpl-cli/cpl/cli/model/cpl_structure_model.py new file mode 100644 index 00000000..c9aa02f4 --- /dev/null +++ b/src/cpl-cli/cpl/cli/model/cpl_structure_model.py @@ -0,0 +1,131 @@ +import inspect +import json +from pathlib import Path +from typing import Any, Dict, List, Optional, Type, TypeVar + +T = TypeVar("T", bound="CPLStructureModel") + + +class CPLStructureModel: + def __init__(self, path: Optional[str] = None): + self._path = path + + @property + def path(self) -> Optional[str]: + return self._path + + @classmethod + def from_file(cls: Type[T], path: Path | str) -> T: + if isinstance(path, str): + path = Path(path) + + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return cls.from_json(data, path=path) + + @classmethod + def from_json(cls: Type[T], data: Dict[str, Any], path: Optional[Path | str] = None) -> T: + if isinstance(path, str): + path = Path(path) + + sig = inspect.signature(cls.__init__) + kwargs: Dict[str, Any] = {} + for name, param in list(sig.parameters.items())[1:]: + if name == "path": + kwargs[name] = path + continue + + if name in data: + kwargs[name] = data[name] + continue + + priv = "_" + name + if priv in data: + kwargs[name] = data[priv] + continue + + camel = _self_or_cls_snake_to_camel(name) + if camel in data: + kwargs[name] = data[camel] + continue + + if param.default is not inspect._empty: + kwargs[name] = param.default + continue + + raise KeyError(f"Missing required field '{name}' for {cls.__name__}.") + + return cls(**kwargs) + + def to_json(self) -> Dict[str, Any]: + result: Dict[str, Any] = {} + for key, value in self.__dict__.items(): + if not key.startswith("_") or key == "_path": + continue + out_key = _self_or_cls_snake_to_camel(key[1:]) + result[out_key] = value + return result + + def save(self): + if not self._path: + raise ValueError("Cannot save model without a path.") + + with open(self._path, "w", encoding="utf-8") as f: + json.dump(self.to_json(), f, indent=2) + + @staticmethod + def _require_str(value, field: str, allow_empty: bool = True) -> str: + if not isinstance(value, str): + raise TypeError(f"{field} must be of type str") + if not allow_empty and not value.strip(): + raise ValueError(f"{field} must not be empty") + return value + + @staticmethod + def _require_optional_non_empty_str(value, field: str) -> Optional[str]: + if value is None: + return None + if not isinstance(value, str): + raise TypeError(f"{field} must be str or None") + s = value.strip() + if not s: + raise ValueError(f"{field} must not be empty when set") + return s + + @staticmethod + def _require_list_of_str(value, field: str) -> List[str]: + if not isinstance(value, list): + raise TypeError(f"{field} must be a list") + out: List[str] = [] + for i, v in enumerate(value): + if not isinstance(v, str): + raise TypeError(f"{field}[{i}] must be of type str") + s = v.strip() + if s: + out.append(s) + + seen = set() + uniq: List[str] = [] + for s in out: + if s not in seen: + seen.add(s) + uniq.append(s) + return uniq + + @staticmethod + def _require_dict_str_str(value, field: str) -> Dict[str, str]: + if not isinstance(value, dict): + raise TypeError(f"{field} must be a dict") + out: Dict[str, str] = {} + for k, v in value.items(): + if not isinstance(k, str) or not k.strip(): + raise TypeError(f"Keys in {field} must be non-empty strings") + if not isinstance(v, str) or not v.strip(): + raise TypeError(f"Values in {field} must be non-empty strings") + out[k.strip()] = v.strip() + return out + + +def _self_or_cls_snake_to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:]) diff --git a/src/cpl-cli/cpl/cli/model/project.py b/src/cpl-cli/cpl/cli/model/project.py new file mode 100644 index 00000000..1944a48f --- /dev/null +++ b/src/cpl-cli/cpl/cli/model/project.py @@ -0,0 +1,178 @@ +import re +from typing import Optional, List, Dict +from urllib.parse import urlparse + +from cpl.cli.model.cpl_structure_model import CPLStructureModel + + +class Project(CPLStructureModel): + _ALLOWED_TYPES = {"application", "library"} + _SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$") + + @staticmethod + def new(path: str, name: str, project_type: str) -> "Project": + return Project( + path, + name, + "0.1.0", + project_type, + "", + "", + "", + "", + [], + {}, + {}, + [], + None, + "src", + ) + + def __init__( + self, + path: str, + name: str, + version: str, + type: str, + license: str, + author: str, + description: str, + homepage: str, + keywords: List[str], + dependencies: Dict[str, str], + dev_dependencies: Dict[str, str], + references: List[str], + main: Optional[str], + directory: str, + ): + CPLStructureModel.__init__(self, path) + + self._name = name + self._version = version + self._type = type + self._license = license + self._author = author + self._description = description + self._homepage = homepage + self._keywords = keywords + self._dependencies = dependencies + self._dev_dependencies = dev_dependencies + self._references = references + self._main = main + self._directory = directory + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + self._name = self._require_str(value, "name", allow_empty=False).strip() + + @property + def version(self) -> str: + return self._version + + @version.setter + def version(self, value: str): + value = self._require_str(value, "version", allow_empty=False).strip() + if not self._SEMVER_RE.match(value): + raise ValueError("version must follow SemVer X.Y.Z (optionally with -/+)") + self._version = value + + @property + def type(self) -> str: + return self._type + + @type.setter + def type(self, value: str): + value = self._require_str(value, "type", allow_empty=False).strip() + if value not in self._ALLOWED_TYPES: + allowed = ", ".join(sorted(self._ALLOWED_TYPES)) + raise ValueError(f"type must be one of: {allowed}") + self._type = value + + @property + def license(self) -> str: + return self._license + + @license.setter + def license(self, value: str): + self._license = self._require_str(value, "license", allow_empty=True).strip() + + @property + def author(self) -> str: + return self._author + + @author.setter + def author(self, value: str): + self._author = self._require_str(value, "author", allow_empty=True).strip() + + @property + def description(self) -> str: + return self._description + + @description.setter + def description(self, value: str): + self._description = self._require_str(value, "description", allow_empty=True).strip() + + @property + def homepage(self) -> str: + return self._homepage + + @homepage.setter + def homepage(self, value: str): + value = self._require_str(value, "homepage", allow_empty=True).strip() + if value: + parsed = urlparse(value) + if parsed.scheme not in ("http", "https") or not parsed.netloc: + raise ValueError("homepage must be a valid http/https URL") + self._homepage = value + + @property + def keywords(self) -> List[str]: + return self._keywords + + @keywords.setter + def keywords(self, value: List[str]): + self._keywords = self._require_list_of_str(value, "keywords") + + @property + def dependencies(self) -> Dict[str, str]: + return self._dependencies + + @dependencies.setter + def dependencies(self, value: Dict[str, str]): + self._dependencies = self._require_dict_str_str(value, "dependencies") + + @property + def dev_dependencies(self) -> Dict[str, str]: + return self._dev_dependencies + + @dev_dependencies.setter + def dev_dependencies(self, value: Dict[str, str]): + self._dev_dependencies = self._require_dict_str_str(value, "devDependencies") + + @property + def references(self) -> List[str]: + return self._references + + @references.setter + def references(self, value: List[str]): + self._references = self._require_list_of_str(value, "references") + + @property + def main(self) -> Optional[str]: + return self._main + + @main.setter + def main(self, value: Optional[str]): + self._main = self._require_optional_non_empty_str(value, "main") + + @property + def directory(self) -> str: + return self._directory + + @directory.setter + def directory(self, value: str): + self._directory = self._require_str(value, "directory", allow_empty=False).strip() diff --git a/src/cpl-cli/cpl/cli/model/workspace.py b/src/cpl-cli/cpl/cli/model/workspace.py new file mode 100644 index 00000000..842ec47c --- /dev/null +++ b/src/cpl-cli/cpl/cli/model/workspace.py @@ -0,0 +1,62 @@ +from typing import Optional, List, Dict + +from cpl.cli.model.cpl_structure_model import CPLStructureModel + + +class Workspace(CPLStructureModel): + @staticmethod + def new(path: str, name: str) -> "Workspace": + return Workspace( + path=path, + name=name, + projects=[], + default_project=None, + scripts={}, + ) + + def __init__( + self, + path: str, + name: str, + projects: List[str], + default_project: Optional[str], + scripts: Dict[str, str], + ): + CPLStructureModel.__init__(self, path) + + self._name = name + self._projects = projects + self._default_project = default_project + self._scripts = scripts + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + self._name = self._require_str(value, "name", allow_empty=False).strip() + + @property + def projects(self) -> List[str]: + return self._projects + + @projects.setter + def projects(self, value: List[str]): + self._projects = self._require_list_of_str(value, "projects") + + @property + def default_project(self) -> Optional[str]: + return self._default_project + + @default_project.setter + def default_project(self, value: Optional[str]): + self._default_project = self._require_optional_non_empty_str(value, "defaultProject") + + @property + def scripts(self) -> Dict[str, str]: + return self._scripts + + @scripts.setter + def scripts(self, value: Dict[str, str]): + self._scripts = self._require_dict_str_str(value, "scripts") diff --git a/src/cpl-cli/cpl/cli/utils/__init__.py b/src/cpl-cli/cpl/cli/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-cli/cpl/cli/utils/custom_command.py b/src/cpl-cli/cpl/cli/utils/custom_command.py new file mode 100644 index 00000000..866fd186 --- /dev/null +++ b/src/cpl-cli/cpl/cli/utils/custom_command.py @@ -0,0 +1,18 @@ +import subprocess + +import click + + +def script_command(cli_group, name, command): + + @cli_group.command(name) + @click.argument("args", nargs=-1) + def _run_script(args): + click.echo(f"Running script: {name}") + try: + cmd = command.split() + list(args) + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + click.echo(f"Script '{name}' failed with exit code {e.returncode}", err=True) + except FileNotFoundError: + click.echo(f"Command not found: {command.split()[0]}", err=True) diff --git a/src/cpl-cli/cpl/cli/utils/json.py b/src/cpl-cli/cpl/cli/utils/json.py new file mode 100644 index 00000000..f39e0400 --- /dev/null +++ b/src/cpl-cli/cpl/cli/utils/json.py @@ -0,0 +1,14 @@ +import json +from pathlib import Path + + +def load_project_json(path: Path) -> dict: + if not path.exists(): + return {} + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def save_project_json(path: Path, data: dict): + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) diff --git a/src/cpl-cli/cpl/cli/utils/pip.py b/src/cpl-cli/cpl/cli/utils/pip.py new file mode 100644 index 00000000..5c51dd16 --- /dev/null +++ b/src/cpl-cli/cpl/cli/utils/pip.py @@ -0,0 +1,50 @@ +import subprocess +from pathlib import Path + +from cpl.cli.utils.venv import ensure_venv, get_venv_pip +from cpl.core.console import Console + + +class Pip: + + @staticmethod + def normalize_dep(name: str, raw: str) -> str: + raw = raw.strip() + if not raw: + return name + + table = { + "!": "!=", + ">": ">=", + ">>": ">", + "<": "<=", + "<<": "<", + "~": "~=", + "=": "===", + } + + op = "==" + for prefix, pip_op in table.items(): + if raw.startswith(prefix): + op = pip_op + raw = raw[len(prefix) :] + break + + return f"{name}{op}{raw}" + + @staticmethod + def command(project_path: str, command: str, *args,verbose:bool=False): + venv_path = ensure_venv(Path(project_path)) + pip = get_venv_pip(venv_path) + + if verbose: + Console.write_line() + + subprocess.run( + f"{pip} {command} {''.join(args)}", + shell=True, + check=True, + stdin=subprocess.DEVNULL if not verbose else None, + stdout=subprocess.DEVNULL if not verbose else None, + stderr=subprocess.STDOUT if not verbose else None, + ) \ No newline at end of file diff --git a/src/cpl-cli/cpl/cli/utils/prompt.py b/src/cpl-cli/cpl/cli/utils/prompt.py new file mode 100644 index 00000000..e90e7708 --- /dev/null +++ b/src/cpl-cli/cpl/cli/utils/prompt.py @@ -0,0 +1,20 @@ +import click + + +class SmartChoice(click.Choice): + + def __init__(self, choices: list, aliases: dict = None): + click.Choice.__init__(self, choices, case_sensitive=False) + + self._aliases = {c: c[0].lower() for c in choices if len(c) > 0} + if aliases: + self._aliases.update({k: v.lower() for k, v in aliases.items()}) + + if any([x[0].lower in self._aliases for x in choices if len(x) > 1]): + raise ValueError("Alias conflict with first letters of choices") + + def convert(self, value, param, ctx): + val_lower = value.lower() + if val_lower in self._aliases.values(): + value = [k for k, v in self._aliases.items() if v == val_lower][0] + return super().convert(value, param, ctx) diff --git a/src/cpl-cli/cpl/cli/utils/structure.py b/src/cpl-cli/cpl/cli/utils/structure.py new file mode 100644 index 00000000..17266631 --- /dev/null +++ b/src/cpl-cli/cpl/cli/utils/structure.py @@ -0,0 +1,30 @@ +import json +from pathlib import Path + +from cpl.cli.model.project import Project +from cpl.cli.model.workspace import Workspace +from cpl.core.console import Console + + +def resolve_project(path: Path, name: str | None) -> Project: + project_file = path / "cpl.project.json" + if project_file.exists(): + return Project.from_file(project_file) + + workspace_file = path / "cpl.workspace.json" + if workspace_file.exists(): + workspace = Workspace.from_file(workspace_file) + if name: + for p in workspace.projects: + project = Project.from_file(p) + if project.name == name: + return project + + elif workspace.default_project: + for p in workspace.projects: + project = Project.from_file(p) + if project.name == workspace.default_project: + return project + + Console.error(f"Could not find project file '{path}'") + exit(1) diff --git a/src/cpl-cli/cpl/cli/utils/venv.py b/src/cpl-cli/cpl/cli/utils/venv.py new file mode 100644 index 00000000..b34b84f7 --- /dev/null +++ b/src/cpl-cli/cpl/cli/utils/venv.py @@ -0,0 +1,44 @@ +import os +import sys +import venv +from pathlib import Path + +from cpl.core.configuration import Configuration +from cpl.core.console import Console + + +def ensure_venv(start_path: Path | None = None) -> Path: + start_path = start_path or Path.cwd() + workspace_path = Configuration.get("workspace_path") + + if workspace_path is not None: + workspace_path = Path(os.path.dirname(workspace_path)) + + ws_venv = workspace_path / ".venv" + if ws_venv.exists(): + return ws_venv + + for parent in [start_path, *start_path.parents]: + venv_path = parent / ".venv" + if venv_path.exists(): + return venv_path + + if workspace_path is not None: + venv_path = workspace_path / ".venv" + else: + venv_path = start_path / ".venv" + + Console.write_line(f"Creating virtual environment at {venv_path}...") + venv.EnvBuilder(with_pip=True).create(venv_path) + return venv_path + + +def get_venv_python(venv_path: Path) -> Path: + if sys.platform == "win32": + return venv_path / "Scripts" / "python.exe" + return venv_path / "bin" / "python" + + +def get_venv_pip(venv_path: Path) -> str: + python_exe = get_venv_python(venv_path) + return f"{python_exe} -m pip" diff --git a/src/cpl-cli/pyproject.toml b/src/cpl-cli/pyproject.toml new file mode 100644 index 00000000..afbbe062 --- /dev/null +++ b/src/cpl-cli/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools>=70.1.0", "wheel>=0.43.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "cpl-cli" +version = "2024.7.0" +description = "CPL cli" +readme = "CPL cli package" +requires-python = ">=3.12" +license = "MIT" +authors = [ + { name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" } +] +keywords = ["cpl", "cli", "backend", "shared", "library"] + +dynamic = ["dependencies", "optional-dependencies"] + +[project.urls] +Homepage = "https://www.sh-edraft.de" + +[tool.setuptools.packages.find] +where = ["."] +include = ["cpl*"] + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.txt"] } +optional-dependencies.dev = { file = ["requirements.dev.txt"] } + diff --git a/src/cpl-cli/requirements.dev.txt b/src/cpl-cli/requirements.dev.txt new file mode 100644 index 00000000..e7664b42 --- /dev/null +++ b/src/cpl-cli/requirements.dev.txt @@ -0,0 +1 @@ +black==25.1.0 \ No newline at end of file diff --git a/src/cpl-cli/requirements.txt b/src/cpl-cli/requirements.txt new file mode 100644 index 00000000..a8244b30 --- /dev/null +++ b/src/cpl-cli/requirements.txt @@ -0,0 +1 @@ +cpl-core \ No newline at end of file diff --git a/src/cpl-core/cpl.project.json b/src/cpl-core/cpl.project.json new file mode 100644 index 00000000..66ebca33 --- /dev/null +++ b/src/cpl-core/cpl.project.json @@ -0,0 +1,24 @@ +{ + "name": "cpl-core", + "version": "0.1.0", + "type": "library", + "license": "MIT", + "author": "Sven Heidemann", + "description": "CLI for the CPL library", + "homepage": "", + "keywords": [], + "dependencies": { + "art": "~6.5", + "colorama": "~0.4.6", + "tabulate": "~0.9.0", + "termcolor": "~3.1.0", + "pynput": "~1.8.1", + "croniter": "~6.0.0" + }, + "devDependencies": { + "black": "25.1.0" + }, + "references": [], + "main": null, + "directory": "cpl/core" +} \ No newline at end of file