cpl new use venv & install deps on creation
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 7s

This commit is contained in:
2025-10-19 14:39:05 +02:00
parent 76b44ca517
commit bb62798c37
19 changed files with 127 additions and 41 deletions

View File

@@ -3,8 +3,8 @@ from datetime import datetime
from cpl.core.console import Console from cpl.core.console import Console
from cpl.core.time.cron import Cron from cpl.core.time.cron import Cron
from cpl.dependency.hosted.cronjob import CronjobABC from cpl.core.service.cronjob import CronjobABC
from cpl.dependency.hosted.hosted_service import HostedService from cpl.core.service.hosted_service import HostedService
class Hosted(HostedService): class Hosted(HostedService):

View File

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

View File

@@ -0,0 +1,13 @@
from cpl.core.console import Console
from cpl.core.service import HostedService
class <Name>(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!")

View File

@@ -1,8 +1,11 @@
import os
import subprocess import subprocess
from pathlib import Path
import click import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.const import PIP_URL
from cpl.cli.utils.pip import Pip from cpl.cli.utils.pip import Pip
from cpl.cli.utils.structure import Structure from cpl.cli.utils.structure import Structure
from cpl.core.console import Console 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}':") Console.write_line(f"Installing {package} to '{project.name}':")
try: try:
Pip.command( 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, package,
verbose=verbose, verbose=verbose,
path=project.path, path=Path(project.path).parent,
) )
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
Console.error(f"Failed to install {package}: exit code {e.returncode}") Console.error(f"Failed to install {package}: exit code {e.returncode}")

View File

@@ -3,6 +3,7 @@ import subprocess
import click import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.const import PIP_URL
from cpl.cli.utils.pip import Pip from cpl.cli.utils.pip import Pip
from cpl.cli.utils.structure import Structure from cpl.cli.utils.structure import Structure
from cpl.core.console import Console 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}':") Console.write_line(f"Updating {package} to '{project.name}':")
try: try:
Pip.command( Pip.command(
"install --upgrade --extra-index-url https://git.sh-edraft.de/api/packages/sh-edraft.de/pypi/simple/", f"install --upgrade --extra-index-url {PIP_URL}" f"{Pip.normalize_dep(package, old_spec)}",
f"{Pip.normalize_dep(package, old_spec)}",
verbose=verbose, verbose=verbose,
path=project.path, path=project.path,
) )

View File

@@ -3,8 +3,9 @@ from pathlib import Path
import click import click
from cpl.cli.const import PROJECT_TYPES, PROJECT_TYPES_SHORT 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.structure import Structure
from cpl.cli.utils.venv import ensure_venv
from cpl.core.console import Console from cpl.core.console import Console
@@ -12,11 +13,14 @@ from cpl.core.console import Console
@click.argument("target", required=False) @click.argument("target", required=False)
@click.argument("name", required=False) @click.argument("name", required=False)
def init(target: str, name: str): def init(target: str, name: str):
workspace = None
project = None
if target is None: if target is None:
Console.write_line("CPL Init Wizard") Console.write_line("CPL Init Wizard")
target = click.prompt( target = click.prompt(
"What do you want to initialize?", "What do you want to initialize?",
type=SmartChoice(["workspace"] + PROJECT_TYPES, {"workspace": "ws"}), type=ProjectType,
show_choices=True, 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] target = [pt for pt in PROJECT_TYPES if pt.startswith(target)][0]
if target in ["workspace", "ws"]: 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: elif target in PROJECT_TYPES:
workspace = Structure.find_workspace_in_path(Path(name).parent) 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: else:
Console.error(f"Unknown target '{target}'") Console.error(f"Unknown target '{target}'")
raise SystemExit(1) raise SystemExit(1)
ensure_venv(Path((workspace or project).path).parent)

View File

@@ -2,16 +2,19 @@ import os
from pathlib import Path from pathlib import Path
import click import click
from cpl.cli import cli as clim from cpl.cli import cli as clim
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.const import PROJECT_TYPES, PROJECT_TYPES_SHORT from cpl.cli.const import PROJECT_TYPES, PROJECT_TYPES_SHORT
from cpl.cli.model.workspace import Workspace 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.structure import Structure
from cpl.cli.utils.venv import ensure_venv
from cpl.core.console import Console from cpl.core.console import Console
@cli.command("new", aliases=["n"]) @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.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("--name", "in_name", type=click.STRING, help="Name of the workspace or project to create.")
@click.option( @click.option(
@@ -23,16 +26,17 @@ from cpl.core.console import Console
) )
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @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: def new(type: str, name: str, in_name: str | None, project: list[str] | None, verbose: bool) -> None:
workspace_created = False
path = Path(name).parent path = Path(name).parent
project_name = in_name or Path(name).stem project_name = in_name or Path(name).stem
if type in ["workspace", "ws"]: if type in ["workspace", "ws"]:
Structure.init_workspace(name, project_name) Structure.init_workspace(name, project_name)
workspace = Workspace.from_file(Path(name) / "cpl.workspace.json") workspace = Workspace.from_file(Path(name) / "cpl.workspace.json")
workspace_created = True ensure_venv(Path(name))
if project is None or len(project) != 2:
return
if len(project) == 2:
type = project[0] type = project[0]
if type not in PROJECT_TYPES + PROJECT_TYPES_SHORT: if type not in PROJECT_TYPES + PROJECT_TYPES_SHORT:
raise ValueError(f"Unknown project type '{type}'") raise ValueError(f"Unknown project type '{type}'")
@@ -40,7 +44,7 @@ def new(type: str, name: str, in_name: str | None, project: list[str] | None, ve
path = Path(workspace.path).parent / Path(project[1]).parent path = Path(workspace.path).parent / Path(project[1]).parent
project_name = Path(project[1]).stem project_name = Path(project[1]).stem
workspace = Structure.find_workspace_in_path(path) workspace = Structure.find_workspace_in_path(path, with_parents=False)
if workspace is None: if workspace is None:
Console.error("No workspace found. Please run 'cpl init workspace' first.") Console.error("No workspace found. Please run 'cpl init workspace' first.")
raise SystemExit(1) 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}'") raise ValueError(f"Unsupported project type '{type}'")
Structure.create_project(path, type, project_name, workspace, verbose) 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.default_project = project_name
workspace.save() workspace.save()

View File

@@ -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] PROJECT_TYPES_SHORT = [x[0] for x in PROJECT_TYPES]
PIP_URL = "https://git.sh-edraft.de/api/packages/sh-edraft.de/pypi/simple/"

View File

@@ -1,4 +1,3 @@
import sys
from pathlib import Path from pathlib import Path
from cpl.cli.cli import cli 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.model.workspace import Workspace
from cpl.cli.utils.custom_command import script_command from cpl.cli.utils.custom_command import script_command
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.console import Console
def _load_workspace(path: str) -> Workspace | None: def _load_workspace(path: str) -> Workspace | None:
@@ -74,12 +74,16 @@ def configure():
def main(): def main():
prepare() prepare()
configure() configure()
try:
cli() cli()
finally:
Console.write_line()
if __name__ == "__main__": if __name__ == "__main__":
main() main()
# (( # ((
# ( `) # ( `)
# ; / , # ; / ,

View File

@@ -52,7 +52,7 @@ class Workspace(CPLStructureModel):
@property @property
def project_names(self) -> List[str]: 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 @property
def default_project(self) -> Optional[str]: def default_project(self) -> Optional[str]:

View File

@@ -97,13 +97,12 @@ class Pip:
return spec.replace(package_name, "").strip() or None return spec.replace(package_name, "").strip() or None
@staticmethod @staticmethod
def command(command: str, *args, verbose: bool = False, path: str = None): def command(command: str, *args, verbose: bool = False, path: Path = None):
if path is not None and Path(path).is_file(): if path is not None and path.is_file():
path = os.path.dirname(path) 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) pip = get_venv_pip(venv_path)
if verbose: if verbose:
Console.write_line() Console.write_line()
Console.write_line(f"Running: {pip} {command} {''.join(args)}") Console.write_line(f"Running: {pip} {command} {''.join(args)}")
@@ -111,6 +110,7 @@ class Pip:
subprocess.run( subprocess.run(
[*pip.split(), *command.split(), *args], [*pip.split(), *command.split(), *args],
check=True, check=True,
cwd=path,
stdin=subprocess.DEVNULL if not verbose else None, stdin=subprocess.DEVNULL if not verbose else None,
stdout=subprocess.DEVNULL if not verbose else None, stdout=subprocess.DEVNULL if not verbose else None,
stderr=subprocess.DEVNULL if not verbose else None, stderr=subprocess.DEVNULL if not verbose else None,

View File

@@ -1,5 +1,7 @@
import click import click
from cpl.cli.const import PROJECT_TYPES
class SmartChoice(click.Choice): class SmartChoice(click.Choice):
@@ -18,3 +20,8 @@ class SmartChoice(click.Choice):
if val_lower in self._aliases.values(): if val_lower in self._aliases.values():
value = [k for k, v in self._aliases.items() if v == val_lower][0] value = [k for k, v in self._aliases.items() if v == val_lower][0]
return super().convert(value, param, ctx) 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"})

View File

@@ -5,7 +5,7 @@ from pathlib import Path
import click import click
import cpl.cli as cli from cpl import cli
from cpl.cli.model.project import Project from cpl.cli.model.project import Project
from cpl.cli.model.workspace import Workspace from cpl.cli.model.workspace import Workspace
from cpl.cli.utils.template_renderer import TemplateRenderer from cpl.cli.utils.template_renderer import TemplateRenderer
@@ -13,17 +13,35 @@ from cpl.core.console import Console
class Structure: class Structure:
@staticmethod _dependency_map = {
def find_workspace_in_path(path: Path) -> Workspace | None: "console": [
current_path = path.resolve() "cpl-core",
],
"web": [
"cpl-api",
],
"graphql": [
"cpl-graphql",
],
"library": [
"cpl-core",
],
"service": [
"cpl-core",
]
}
Console.write_line(*([current_path] + list(current_path.parents))) @staticmethod
for parent in [current_path] + list(current_path.parents): 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" workspace_file = parent / "cpl.workspace.json"
Console.write_line(workspace_file)
if workspace_file.exists() and workspace_file.is_file(): if workspace_file.exists() and workspace_file.is_file():
ws = Workspace.from_file(workspace_file) ws = Workspace.from_file(workspace_file)
Console.error(ws.name)
return ws return ws
return None return None
@@ -95,6 +113,7 @@ class Structure:
workspace.save() workspace.save()
Console.write_line(f"Created workspace '{name}'") Console.write_line(f"Created workspace '{name}'")
return workspace
@staticmethod @staticmethod
def init_project(rel_path: str, name: str, project_type: str, workspace: Workspace | None, verbose=False): 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") path = Path(rel_path) / Path("cpl.project.json")
if path.exists(): if path.exists():
Console.write_line("cpl.project.json already exists.") Console.error("cpl.project.json already exists.")
return raise SystemExit(1)
project = Project.new(str(path), name, project_type) project = Project.new(str(path), name, project_type)
@@ -120,6 +139,16 @@ class Structure:
project.save() 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: if workspace is not None:
rel_path = str(path.resolve().absolute().relative_to(Path(workspace.path).parent)) rel_path = str(path.resolve().absolute().relative_to(Path(workspace.path).parent))
if rel_path not in workspace.projects: 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"Registered '{name}' in workspace.json")
Console.write_line(f"Created {project_type} project '{name}'") Console.write_line(f"Created {project_type} project '{name}'")
return project
@staticmethod @staticmethod
def create_project(path: Path, project_type: str, name: str, workspace: Workspace | None, verbose=False): def create_project(path: Path, project_type: str, name: str, workspace: Workspace | None, verbose=False):

View File

@@ -9,7 +9,8 @@ from cpl.core.console import Console
def ensure_venv(start_path: Path | None = None) -> Path: def ensure_venv(start_path: Path | None = None) -> Path:
start_path = start_path or Path.cwd() 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: if workspace is not None:
workspace = Path(os.path.dirname(workspace.path)) workspace = Path(os.path.dirname(workspace.path))
@@ -28,7 +29,7 @@ def ensure_venv(start_path: Path | None = None) -> Path:
else: else:
venv_path = start_path / ".venv" 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) venv.EnvBuilder(with_pip=True).create(venv_path)
return venv_path return venv_path

View File

@@ -16,6 +16,9 @@ keywords = ["cpl", "cli", "backend", "shared", "library"]
dynamic = ["dependencies", "optional-dependencies"] dynamic = ["dependencies", "optional-dependencies"]
[project.scripts]
cpl = "cpl.cli.main:main"
[project.urls] [project.urls]
Homepage = "https://www.sh-edraft.de" Homepage = "https://www.sh-edraft.de"