Renamed project dirs
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s

This commit is contained in:
2025-10-11 09:32:13 +02:00
parent f1aaaf2a5b
commit 90ff8d466d
319 changed files with 0 additions and 0 deletions

21
src/cli/cpl.project.json Normal file
View File

@@ -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.9"
},
"references": [
"../cpl/cpl.project.json"
],
"main": "cpl/cli/main.py",
"directory": "cpl/cli"
}

View File

@@ -0,0 +1,2 @@
__version__ = "1.0.0"
__version__ = "1.0.0"

33
src/cli/cpl/cli/cli.py Normal file
View File

@@ -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(): ...

View File

View File

@@ -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}'")

View File

@@ -0,0 +1,60 @@
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.argument("package", type=click.STRING, 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 install(package: str, project_path: str, project_name: str, dev: bool, verbose: bool):
project = resolve_project(Path(project_path), project_name)
if package is not None:
Console.write_line(f"Installing {package} to '{project.name}':")
try:
Pip.command("install", package, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to install {package}: exit code {e.returncode}")
package_name = Pip.get_package_without_version(package)
installed_version = Pip.get_package_version(package_name, path=project_path)
if installed_version is None:
Console.error(f"Package '{package_name}' not found after installation.")
return
project.dependencies[package_name] = Pip.apply_prefix(installed_version, Pip.get_package_full_version(package))
project.save()
Console.write_line(f"Added {package_name}~{installed_version} to project dependencies.")
return
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("install", dep, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to install {dep}: exit code {e.returncode}")

View File

@@ -0,0 +1,40 @@
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("install", package, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to uninstall {package}: exit code {e.returncode}")
return
if package in deps:
del deps[package]
project.save()
Console.write_line(f"Removed {package} from project dependencies.")

View File

@@ -0,0 +1,81 @@
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("update", aliases=["u"])
@click.argument("package", type=click.STRING, 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 update(package: str, 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 = project.dev_dependencies
if package is not None:
if package not in deps:
Console.error(f"Package '{package}' not installed.")
return
old_spec = deps[package]
Console.write_line(f"Updating {package} to '{project.name}':")
try:
Pip.command(
"install --upgrade",
f"{Pip.normalize_dep(package, old_spec)}",
verbose=verbose,
path=project_path,
)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to install {package}: exit code {e.returncode}")
return
installed_version = Pip.get_package_version(package, path=project_path)
if installed_version is None:
Console.error(f"Package '{package}' not found after update.")
return
deps[package] = Pip.apply_prefix(installed_version, old_spec)
project.save()
Console.write_line(f"Updated {package} to {deps[package]}")
return
if not deps:
Console.error("No dependencies to install.")
return
Console.write_line(f"Updating dependencies for '{project.name}':")
for name, version in list(deps.items()):
dep = Pip.normalize_dep(name, version)
Console.write_line(f" -> {dep}")
try:
Pip.command("install --upgrade", dep, verbose=verbose, path=project_path)
except subprocess.CalledProcessError as e:
Console.error(f"Failed to update {dep}: exit code {e.returncode}")
return
installed_version = Pip.get_package_version(name, path=project_path)
if installed_version is None:
Console.error(f"Package '{name}' not found after update.")
continue
deps[name] = Pip.apply_prefix(installed_version, version)
project.save()

View File

@@ -0,0 +1,27 @@
import platform
import cpl
from cpl.cli.cli import cli
from cpl.cli.utils.pip import Pip
from cpl.core.console import Console, ForegroundColorEnum
@cli.command("version", aliases=["v"])
def version():
Console.set_foreground_color(ForegroundColorEnum.yellow)
Console.banner("CPL CLI")
Console.set_foreground_color(ForegroundColorEnum.default)
Console.write_line()
Console.write_line(f"CPL CLI: {getattr(cpl.cli, '__version__', "1.0")}")
Console.write_line(f"Python: {platform.python_version()}")
Console.write_line(f"PIP: {Pip.get_pip_version()}")
Console.write_line(f"OS: {platform.system()} {platform.release()}")
Console.write_line("\nCPL Packages:\n")
cpl_packages = {n: v for n, v in Pip.get_packages().items() if n.startswith("cpl-")}
if len(cpl_packages) == 0:
Console.write_line("No CPL packages installed")
return
Console.table(["Package", "Version"], [[n, v] for n, v in cpl_packages.items()])

2
src/cli/cpl/cli/const.py Normal file
View File

@@ -0,0 +1,2 @@
PROJECT_TYPES = ["console", "web", "library", "service"]
PROJECT_TYPES_SHORT = [x[0] for x in PROJECT_TYPES]

82
src/cli/cpl/cli/main.py Normal file
View File

@@ -0,0 +1,82 @@
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.command.update import update
from cpl.cli.command.version import version
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
cli.add_command(version)
# structure
cli.add_command(init)
# cli.add_command(new)
# packaging
cli.add_command(install)
cli.add_command(uninstall)
cli.add_command(update)
# cli.add_command(add)
# cli.add_command(remove)
# run
# cli.add_command(run)
# cli.add_command(start)
def main():
prepare()
configure()
cli()
if __name__ == "__main__":
main()
# ((
# ( `)
# ; / ,
# / \/
# / |
# / ~/
# / ) ) ~ edraft
# ___// | /
# `--' \_~-,

View File

View File

@@ -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:])

View File

@@ -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()

View File

@@ -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")

View File

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,176 @@
import re
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:
_ANY_PREFIX_RE = re.compile(r"(===|~=|==|!=|>=|<=|>>|<<|>|<|!|~|=|\^)")
@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}"
@classmethod
def apply_prefix(cls, installed: str, spec: str = None) -> str:
if spec is None or not spec.strip():
return f"~{installed}"
s = spec.strip()
if "," in s:
return s
m = cls._ANY_PREFIX_RE.search(s)
if not m:
return f"~{installed}"
op = m.group(1)
rest = s[m.end() :].strip()
if "," in rest:
rest = rest.split(",", 1)[0].strip()
if " " in rest:
rest = rest.split()[0]
orig_version = rest
installed_parts = [p for p in installed.split(".") if p != ""]
if orig_version:
orig_parts = [p for p in orig_version.split(".") if p != ""]
trimmed_installed = ".".join(installed_parts[: len(orig_parts)]) or installed
else:
trimmed_installed = installed
pip_to_cpl = {
"==": "",
"!=": "!",
">=": ">",
">": ">>",
"<=": "<",
"<": "<<",
"~=": "~",
"===": "=",
"^": "~",
}
if op in pip_to_cpl:
cpl_op = pip_to_cpl[op]
else:
cpl_op = op
return f"{cpl_op}{trimmed_installed}"
@classmethod
def get_package_without_version(cls, spec: str) -> str:
for sep in ["==", ">=", "<=", ">", "<", "~=", "!="]:
if sep in spec:
return spec.split(sep, 1)[0].strip() or None
return spec.strip() or None
@classmethod
def get_package_full_version(cls, spec: str) -> str | None:
package_name = cls.get_package_without_version(spec)
return spec.replace(package_name, "").strip() or None
@staticmethod
def command(command: str, *args, verbose: bool = False, path: str = None):
venv_path = ensure_venv(Path(path or "./"))
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,
)
@staticmethod
def get_package_version(package: str, path: str = None) -> str | None:
venv_path = ensure_venv(Path(path or "./"))
pip = get_venv_pip(venv_path)
try:
result = subprocess.run(
f"{pip} show {package}",
shell=True,
check=True,
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
)
for line in result.stdout.splitlines():
if line.startswith("Version:"):
return line.split(":", 1)[1].strip()
except subprocess.CalledProcessError:
return None
return None
@staticmethod
def get_packages(path: str = None):
venv_path = ensure_venv(Path(path or "./"))
pip = get_venv_pip(venv_path)
try:
result = subprocess.run(
f"{pip} list --format=freeze",
shell=True,
check=True,
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
)
packages = {}
for line in result.stdout.splitlines():
if "==" in line:
name, version = line.split("==", 1)
packages[name] = version
return packages
except subprocess.CalledProcessError:
return {}
@staticmethod
def get_pip_version(path: str = None) -> str | None:
venv_path = ensure_venv(Path(path or "./"))
pip = get_venv_pip(venv_path)
try:
result = subprocess.run(
f"{pip} --version",
shell=True,
check=True,
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
)
version = result.stdout.split()[1]
return version
except subprocess.CalledProcessError:
return None

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"

29
src/cli/pyproject.toml Normal file
View File

@@ -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"] }

View File

@@ -0,0 +1 @@
black==25.1.0

2
src/cli/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
cpl-core
click==8.3.0