cli #199

Merged
edraft merged 22 commits from cli into dev 2025-10-19 14:40:46 +02:00
19 changed files with 130 additions and 41 deletions
Showing only changes of commit 6d3e435da6 - Show all commits

View File

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

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

View File

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

View File

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

View File

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

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]
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 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()
# ((
# ( `)
# ; / ,

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
import click
from cpl.cli.const import PROJECT_TYPES
class SmartChoice(click.Choice):
@@ -18,3 +20,9 @@ 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"})

View File

@@ -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,17 @@ 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 +160,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):

View File

@@ -9,7 +9,9 @@ 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 +30,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

View File

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