From bb62798c37f26ea46e8d8a2881ca4d8eebd87253 Mon Sep 17 00:00:00 2001 From: edraft Date: Sun, 19 Oct 2025 14:39:05 +0200 Subject: [PATCH] cpl new use venv & install deps on creation --- example/general/src/hosted_service.py | 4 +- .../cli/.cpl/generate/cron_job.py.schematic | 9 ++++ .../.cpl/generate/hosted_service.py.schematic | 13 +++++ src/cli/cpl/cli/command/package/install.py | 7 ++- src/cli/cpl/cli/command/package/update.py | 4 +- src/cli/cpl/cli/command/structure/init.py | 16 ++++-- src/cli/cpl/cli/command/structure/new.py | 28 +++++++---- src/cli/cpl/cli/const.py | 4 +- src/cli/cpl/cli/main.py | 8 ++- src/cli/cpl/cli/model/workspace.py | 2 +- src/cli/cpl/cli/utils/pip.py | 8 +-- src/cli/cpl/cli/utils/prompt.py | 7 +++ src/cli/cpl/cli/utils/structure.py | 50 +++++++++++++++---- src/cli/cpl/cli/utils/venv.py | 5 +- src/cli/pyproject.toml | 3 ++ .../cpl/core/service}/__init__.py | 0 .../cpl/core/service}/cronjob.py | 0 .../cpl/core/service}/hosted_service.py | 0 .../cpl/core/service}/startup_task.py | 0 19 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 src/cli/cpl/cli/.cpl/generate/cron_job.py.schematic create mode 100644 src/cli/cpl/cli/.cpl/generate/hosted_service.py.schematic rename src/{dependency/cpl/dependency/hosted => core/cpl/core/service}/__init__.py (100%) rename src/{dependency/cpl/dependency/hosted => core/cpl/core/service}/cronjob.py (100%) rename src/{dependency/cpl/dependency/hosted => core/cpl/core/service}/hosted_service.py (100%) rename src/{dependency/cpl/dependency/hosted => core/cpl/core/service}/startup_task.py (100%) diff --git a/example/general/src/hosted_service.py b/example/general/src/hosted_service.py index f2fbf762..15d91753 100644 --- a/example/general/src/hosted_service.py +++ b/example/general/src/hosted_service.py @@ -3,8 +3,8 @@ from datetime import datetime from cpl.core.console import Console from cpl.core.time.cron import Cron -from cpl.dependency.hosted.cronjob import CronjobABC -from cpl.dependency.hosted.hosted_service import HostedService +from cpl.core.service.cronjob import CronjobABC +from cpl.core.service.hosted_service import HostedService class Hosted(HostedService): diff --git a/src/cli/cpl/cli/.cpl/generate/cron_job.py.schematic b/src/cli/cpl/cli/.cpl/generate/cron_job.py.schematic new file mode 100644 index 00000000..1919b5e1 --- /dev/null +++ b/src/cli/cpl/cli/.cpl/generate/cron_job.py.schematic @@ -0,0 +1,9 @@ +from cpl.core.console import Console +from cpl.core.service import CronjobABC + +class CronJob(CronjobABC): + def __init__(self): + CronjobABC.__init__(self, Cron("*/1 * * * *")) + + async def loop(self): + Console.write_line(f"[{datetime.now()}] Hello, World!") diff --git a/src/cli/cpl/cli/.cpl/generate/hosted_service.py.schematic b/src/cli/cpl/cli/.cpl/generate/hosted_service.py.schematic new file mode 100644 index 00000000..4bd8f6f5 --- /dev/null +++ b/src/cli/cpl/cli/.cpl/generate/hosted_service.py.schematic @@ -0,0 +1,13 @@ +from cpl.core.console import Console +from cpl.core.service import HostedService + + +class (HostedService): + def __init__(self): + HostedService.__init__(self) + + async def start(self): + Console.write_line("Hello, World!") + + async def stop(self): + Console.write_line("Goodbye, World!") diff --git a/src/cli/cpl/cli/command/package/install.py b/src/cli/cpl/cli/command/package/install.py index b06d68f8..c1c2a195 100644 --- a/src/cli/cpl/cli/command/package/install.py +++ b/src/cli/cpl/cli/command/package/install.py @@ -1,8 +1,11 @@ +import os import subprocess +from pathlib import Path import click from cpl.cli.cli import cli +from cpl.cli.const import PIP_URL from cpl.cli.utils.pip import Pip from cpl.cli.utils.structure import Structure from cpl.core.console import Console @@ -20,10 +23,10 @@ def install(package: str, project: str, dev: bool, verbose: bool): Console.write_line(f"Installing {package} to '{project.name}':") try: Pip.command( - "install --extra-index-url https://git.sh-edraft.de/api/packages/sh-edraft.de/pypi/simple/", + f"install --extra-index-url {PIP_URL}", package, verbose=verbose, - path=project.path, + path=Path(project.path).parent, ) except subprocess.CalledProcessError as e: Console.error(f"Failed to install {package}: exit code {e.returncode}") diff --git a/src/cli/cpl/cli/command/package/update.py b/src/cli/cpl/cli/command/package/update.py index e37ed395..6268bdba 100644 --- a/src/cli/cpl/cli/command/package/update.py +++ b/src/cli/cpl/cli/command/package/update.py @@ -3,6 +3,7 @@ import subprocess import click from cpl.cli.cli import cli +from cpl.cli.const import PIP_URL from cpl.cli.utils.pip import Pip from cpl.cli.utils.structure import Structure from cpl.core.console import Console @@ -30,8 +31,7 @@ def update(package: str, project: str, dev: bool, verbose: bool): Console.write_line(f"Updating {package} to '{project.name}':") try: Pip.command( - "install --upgrade --extra-index-url https://git.sh-edraft.de/api/packages/sh-edraft.de/pypi/simple/", - f"{Pip.normalize_dep(package, old_spec)}", + f"install --upgrade --extra-index-url {PIP_URL}" f"{Pip.normalize_dep(package, old_spec)}", verbose=verbose, path=project.path, ) diff --git a/src/cli/cpl/cli/command/structure/init.py b/src/cli/cpl/cli/command/structure/init.py index a8323fa4..ee7c4d7f 100644 --- a/src/cli/cpl/cli/command/structure/init.py +++ b/src/cli/cpl/cli/command/structure/init.py @@ -3,8 +3,9 @@ from pathlib import Path import click from cpl.cli.const import PROJECT_TYPES, PROJECT_TYPES_SHORT -from cpl.cli.utils.prompt import SmartChoice +from cpl.cli.utils.prompt import ProjectType from cpl.cli.utils.structure import Structure +from cpl.cli.utils.venv import ensure_venv from cpl.core.console import Console @@ -12,11 +13,14 @@ from cpl.core.console import Console @click.argument("target", required=False) @click.argument("name", required=False) def init(target: str, name: str): + workspace = None + project = None + 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"}), + type=ProjectType, show_choices=True, ) @@ -24,10 +28,14 @@ def init(target: str, name: str): target = [pt for pt in PROJECT_TYPES if pt.startswith(target)][0] if target in ["workspace", "ws"]: - Structure.init_workspace("./", name or click.prompt("Workspace name", default="my-workspace")) + workspace = Structure.init_workspace("./", name or click.prompt("Workspace name", default="my-workspace")) elif target in PROJECT_TYPES: workspace = Structure.find_workspace_in_path(Path(name).parent) - Structure.init_project("./", name or click.prompt("Project name", default=f"my-{target}"), target, workspace) + project = Structure.init_project( + "./", name or click.prompt("Project name", default=f"my-{target}"), target, workspace + ) else: Console.error(f"Unknown target '{target}'") raise SystemExit(1) + + ensure_venv(Path((workspace or project).path).parent) diff --git a/src/cli/cpl/cli/command/structure/new.py b/src/cli/cpl/cli/command/structure/new.py index be3a74ae..7476d8c0 100644 --- a/src/cli/cpl/cli/command/structure/new.py +++ b/src/cli/cpl/cli/command/structure/new.py @@ -2,16 +2,19 @@ import os from pathlib import Path import click + from cpl.cli import cli as clim from cpl.cli.cli import cli from cpl.cli.const import PROJECT_TYPES, PROJECT_TYPES_SHORT from cpl.cli.model.workspace import Workspace +from cpl.cli.utils.prompt import ProjectType from cpl.cli.utils.structure import Structure +from cpl.cli.utils.venv import ensure_venv from cpl.core.console import Console @cli.command("new", aliases=["n"]) -@click.argument("type", type=click.STRING, required=True) +@click.argument("type", type=ProjectType, required=True) @click.argument("name", type=click.STRING, required=True) @click.option("--name", "in_name", type=click.STRING, help="Name of the workspace or project to create.") @click.option( @@ -23,24 +26,25 @@ from cpl.core.console import Console ) @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") def new(type: str, name: str, in_name: str | None, project: list[str] | None, verbose: bool) -> None: - workspace_created = False path = Path(name).parent project_name = in_name or Path(name).stem if type in ["workspace", "ws"]: Structure.init_workspace(name, project_name) workspace = Workspace.from_file(Path(name) / "cpl.workspace.json") - workspace_created = True + ensure_venv(Path(name)) - if len(project) == 2: - type = project[0] - if type not in PROJECT_TYPES + PROJECT_TYPES_SHORT: - raise ValueError(f"Unknown project type '{type}'") + if project is None or len(project) != 2: + return - path = Path(workspace.path).parent / Path(project[1]).parent - project_name = Path(project[1]).stem + type = project[0] + if type not in PROJECT_TYPES + PROJECT_TYPES_SHORT: + raise ValueError(f"Unknown project type '{type}'") - workspace = Structure.find_workspace_in_path(path) + path = Path(workspace.path).parent / Path(project[1]).parent + project_name = Path(project[1]).stem + + workspace = Structure.find_workspace_in_path(path, with_parents=False) if workspace is None: Console.error("No workspace found. Please run 'cpl init workspace' first.") raise SystemExit(1) @@ -59,6 +63,8 @@ def new(type: str, name: str, in_name: str | None, project: list[str] | None, ve raise ValueError(f"Unsupported project type '{type}'") Structure.create_project(path, type, project_name, workspace, verbose) - if workspace_created: + + ensure_venv(Path((workspace or project).path).parent) + if workspace.default_project is None: workspace.default_project = project_name workspace.save() diff --git a/src/cli/cpl/cli/const.py b/src/cli/cpl/cli/const.py index a5810f03..acb63ff7 100644 --- a/src/cli/cpl/cli/const.py +++ b/src/cli/cpl/cli/const.py @@ -1,2 +1,4 @@ -PROJECT_TYPES = ["console", "web", "library", "service"] +PROJECT_TYPES = ["console", "web", "graphql", "library", "service"] PROJECT_TYPES_SHORT = [x[0] for x in PROJECT_TYPES] + +PIP_URL = "https://git.sh-edraft.de/api/packages/sh-edraft.de/pypi/simple/" diff --git a/src/cli/cpl/cli/main.py b/src/cli/cpl/cli/main.py index 4afb0ca8..baae0e58 100644 --- a/src/cli/cpl/cli/main.py +++ b/src/cli/cpl/cli/main.py @@ -1,4 +1,3 @@ -import sys from pathlib import Path from cpl.cli.cli import cli @@ -17,6 +16,7 @@ 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 +from cpl.core.console import Console def _load_workspace(path: str) -> Workspace | None: @@ -74,12 +74,16 @@ def configure(): def main(): prepare() configure() - cli() + try: + cli() + finally: + Console.write_line() if __name__ == "__main__": main() + # (( # ( `) # ; / , diff --git a/src/cli/cpl/cli/model/workspace.py b/src/cli/cpl/cli/model/workspace.py index b79c6a2e..a80c64d7 100644 --- a/src/cli/cpl/cli/model/workspace.py +++ b/src/cli/cpl/cli/model/workspace.py @@ -52,7 +52,7 @@ class Workspace(CPLStructureModel): @property def project_names(self) -> List[str]: - return [Project.from_file(p).name for p in self._projects if "name" in p] + return [Project.from_file(p).name for p in self._projects] @property def default_project(self) -> Optional[str]: diff --git a/src/cli/cpl/cli/utils/pip.py b/src/cli/cpl/cli/utils/pip.py index 8d9c0798..3b187134 100644 --- a/src/cli/cpl/cli/utils/pip.py +++ b/src/cli/cpl/cli/utils/pip.py @@ -97,13 +97,12 @@ class Pip: return spec.replace(package_name, "").strip() or None @staticmethod - def command(command: str, *args, verbose: bool = False, path: str = None): - if path is not None and Path(path).is_file(): + def command(command: str, *args, verbose: bool = False, path: Path = None): + if path is not None and path.is_file(): path = os.path.dirname(path) - venv_path = ensure_venv(Path(path or "./")) + venv_path = ensure_venv(Path(os.getcwd()) / Path(path or "./")) pip = get_venv_pip(venv_path) - if verbose: Console.write_line() Console.write_line(f"Running: {pip} {command} {''.join(args)}") @@ -111,6 +110,7 @@ class Pip: subprocess.run( [*pip.split(), *command.split(), *args], check=True, + cwd=path, stdin=subprocess.DEVNULL if not verbose else None, stdout=subprocess.DEVNULL if not verbose else None, stderr=subprocess.DEVNULL if not verbose else None, diff --git a/src/cli/cpl/cli/utils/prompt.py b/src/cli/cpl/cli/utils/prompt.py index e90e7708..b5980bf5 100644 --- a/src/cli/cpl/cli/utils/prompt.py +++ b/src/cli/cpl/cli/utils/prompt.py @@ -1,5 +1,7 @@ import click +from cpl.cli.const import PROJECT_TYPES + class SmartChoice(click.Choice): @@ -18,3 +20,8 @@ class SmartChoice(click.Choice): 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) + + def get_metavar(self, param, ctx): + return "|".join([f"({a}){option}" for option,a in self._aliases.items()]) + +ProjectType = SmartChoice(["workspace"] + PROJECT_TYPES, {"workspace": "ws"}) \ No newline at end of file diff --git a/src/cli/cpl/cli/utils/structure.py b/src/cli/cpl/cli/utils/structure.py index e9740916..7662526f 100644 --- a/src/cli/cpl/cli/utils/structure.py +++ b/src/cli/cpl/cli/utils/structure.py @@ -5,7 +5,7 @@ from pathlib import Path import click -import cpl.cli as cli +from cpl import cli from cpl.cli.model.project import Project from cpl.cli.model.workspace import Workspace from cpl.cli.utils.template_renderer import TemplateRenderer @@ -13,17 +13,35 @@ from cpl.core.console import Console class Structure: - @staticmethod - def find_workspace_in_path(path: Path) -> Workspace | None: - current_path = path.resolve() + _dependency_map = { + "console": [ + "cpl-core", + ], + "web": [ + "cpl-api", + ], + "graphql": [ + "cpl-graphql", + ], + "library": [ + "cpl-core", + ], + "service": [ + "cpl-core", + ] + } - Console.write_line(*([current_path] + list(current_path.parents))) - for parent in [current_path] + list(current_path.parents): + @staticmethod + def find_workspace_in_path(path: Path, with_parents=False) -> Workspace | None: + current_path = path.resolve() + paths = [current_path] + if with_parents: + paths.extend(current_path.parents) + + for parent in paths: workspace_file = parent / "cpl.workspace.json" - Console.write_line(workspace_file) if workspace_file.exists() and workspace_file.is_file(): ws = Workspace.from_file(workspace_file) - Console.error(ws.name) return ws return None @@ -95,6 +113,7 @@ class Structure: workspace.save() Console.write_line(f"Created workspace '{name}'") + return workspace @staticmethod def init_project(rel_path: str, name: str, project_type: str, workspace: Workspace | None, verbose=False): @@ -103,8 +122,8 @@ class Structure: path = Path(rel_path) / Path("cpl.project.json") if path.exists(): - Console.write_line("cpl.project.json already exists.") - return + Console.error("cpl.project.json already exists.") + raise SystemExit(1) project = Project.new(str(path), name, project_type) @@ -120,6 +139,16 @@ class Structure: project.save() + from cpl.cli.command.package.install import install + old_cwd = os.getcwd() + os.chdir(Path(workspace.path).parent) + install.callback(f"cpl-cli>={cli.__version__}", project.name, dev=True, verbose=verbose) + if project_type in Structure._dependency_map: + for package in Structure._dependency_map[project_type]: + install.callback(package, project.name, dev=False, verbose=verbose) + + os.chdir(old_cwd) + if workspace is not None: rel_path = str(path.resolve().absolute().relative_to(Path(workspace.path).parent)) if rel_path not in workspace.projects: @@ -130,6 +159,7 @@ class Structure: Console.write_line(f"Registered '{name}' in workspace.json") Console.write_line(f"Created {project_type} project '{name}'") + return project @staticmethod def create_project(path: Path, project_type: str, name: str, workspace: Workspace | None, verbose=False): diff --git a/src/cli/cpl/cli/utils/venv.py b/src/cli/cpl/cli/utils/venv.py index 580f0639..ff451809 100644 --- a/src/cli/cpl/cli/utils/venv.py +++ b/src/cli/cpl/cli/utils/venv.py @@ -9,7 +9,8 @@ from cpl.core.console import Console def ensure_venv(start_path: Path | None = None) -> Path: start_path = start_path or Path.cwd() - workspace = Configuration.get("workspace") + from cpl.cli.utils.structure import Structure + workspace = Structure.find_workspace_in_path(start_path) if workspace is not None: workspace = Path(os.path.dirname(workspace.path)) @@ -28,7 +29,7 @@ def ensure_venv(start_path: Path | None = None) -> Path: else: venv_path = start_path / ".venv" - Console.write_line(f"Creating virtual environment at {venv_path.absolute()}...") + Console.write_line(f"Creating virtual environment at {venv_path.resolve().absolute()}...") venv.EnvBuilder(with_pip=True).create(venv_path) return venv_path diff --git a/src/cli/pyproject.toml b/src/cli/pyproject.toml index afbbe062..92523473 100644 --- a/src/cli/pyproject.toml +++ b/src/cli/pyproject.toml @@ -16,6 +16,9 @@ keywords = ["cpl", "cli", "backend", "shared", "library"] dynamic = ["dependencies", "optional-dependencies"] +[project.scripts] +cpl = "cpl.cli.main:main" + [project.urls] Homepage = "https://www.sh-edraft.de" diff --git a/src/dependency/cpl/dependency/hosted/__init__.py b/src/core/cpl/core/service/__init__.py similarity index 100% rename from src/dependency/cpl/dependency/hosted/__init__.py rename to src/core/cpl/core/service/__init__.py diff --git a/src/dependency/cpl/dependency/hosted/cronjob.py b/src/core/cpl/core/service/cronjob.py similarity index 100% rename from src/dependency/cpl/dependency/hosted/cronjob.py rename to src/core/cpl/core/service/cronjob.py diff --git a/src/dependency/cpl/dependency/hosted/hosted_service.py b/src/core/cpl/core/service/hosted_service.py similarity index 100% rename from src/dependency/cpl/dependency/hosted/hosted_service.py rename to src/core/cpl/core/service/hosted_service.py diff --git a/src/dependency/cpl/dependency/hosted/startup_task.py b/src/core/cpl/core/service/startup_task.py similarity index 100% rename from src/dependency/cpl/dependency/hosted/startup_task.py rename to src/core/cpl/core/service/startup_task.py