cli #199
@@ -46,6 +46,6 @@ def run(project: str, args: list[str], dev: bool, verbose: bool):
|
||||
if verbose:
|
||||
Console.write_line(f" with args {args}...")
|
||||
|
||||
Console.write_line("\n\n") # add some space before output
|
||||
Console.write_line("\n\n")
|
||||
|
||||
subprocess.run([python, executable, *args], cwd=path)
|
||||
|
||||
25
src/cli/cpl/cli/command/project/start.py
Normal file
25
src/cli/cpl/cli/command/project/start.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import click
|
||||
|
||||
from cpl.cli.cli import cli
|
||||
from cpl.cli.utils.live_server.live_server import LiveServer
|
||||
from cpl.cli.utils.structure import get_project_by_name_or_path
|
||||
from cpl.core.console import Console
|
||||
|
||||
|
||||
@cli.command("start", aliases=["s"])
|
||||
@click.argument("args", nargs=-1)
|
||||
@click.option("--project", "-p", type=str)
|
||||
@click.option("--dev", "-d", is_flag=True, help="Use sources instead of build output")
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
||||
def start(project: str, args: list[str], dev: bool, verbose: bool):
|
||||
project = get_project_by_name_or_path(project or "./")
|
||||
if project.main is None:
|
||||
Console.error(f"Project {project.name} has no executable")
|
||||
return
|
||||
|
||||
LiveServer(
|
||||
project,
|
||||
args,
|
||||
dev,
|
||||
verbose,
|
||||
).start()
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from cpl.cli.cli import cli
|
||||
@@ -8,6 +9,7 @@ from cpl.cli.command.package.uninstall import uninstall
|
||||
from cpl.cli.command.package.update import update
|
||||
from cpl.cli.command.project.build import build
|
||||
from cpl.cli.command.project.run import run
|
||||
from cpl.cli.command.project.start import start
|
||||
from cpl.cli.command.structure.init import init
|
||||
from cpl.cli.command.version import version
|
||||
from cpl.cli.model.workspace import Workspace
|
||||
@@ -63,7 +65,7 @@ def configure():
|
||||
# run
|
||||
cli.add_command(build)
|
||||
cli.add_command(run)
|
||||
# cli.add_command(start)
|
||||
cli.add_command(start)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -232,7 +232,7 @@ class Project(CPLStructureModel):
|
||||
|
||||
os.chdir(old_dir)
|
||||
|
||||
def do_build(self, dist: Path, verbose: bool = False, parent: Self = None):
|
||||
def do_build(self, dist: Path, verbose: bool = False, parent: Self = None, silent: bool = False):
|
||||
if isinstance(dist, str):
|
||||
dist = Path(dist)
|
||||
|
||||
@@ -249,7 +249,6 @@ class Project(CPLStructureModel):
|
||||
|
||||
self.build_references(dist, verbose)
|
||||
|
||||
# Console.write_line(f"Building project {self.name}...")
|
||||
def _build():
|
||||
if verbose:
|
||||
Console.write_line(f" Collecting project '{self.name}' files...")
|
||||
@@ -272,6 +271,11 @@ class Project(CPLStructureModel):
|
||||
|
||||
if verbose:
|
||||
Console.write_line(f" Copied {len(files)} files from {rel_dir} to {dist_path}")
|
||||
Console.write_line(" Done!")
|
||||
|
||||
if not silent:
|
||||
Console.write_line(" Done!")
|
||||
|
||||
if silent:
|
||||
_build()
|
||||
return
|
||||
Console.spinner(f"Building project {self.name}...", lambda: _build())
|
||||
|
||||
0
src/cli/cpl/cli/utils/live_server/__init__.py
Normal file
0
src/cli/cpl/cli/utils/live_server/__init__.py
Normal file
51
src/cli/cpl/cli/utils/live_server/file_event_handler.py
Normal file
51
src/cli/cpl/cli/utils/live_server/file_event_handler.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import re
|
||||
import time
|
||||
|
||||
from watchdog.events import FileSystemEventHandler, FileModifiedEvent, FileClosedEvent
|
||||
|
||||
|
||||
class FileEventHandler(FileSystemEventHandler):
|
||||
|
||||
_ignore_patterns = [
|
||||
r".*~$",
|
||||
r".*\.swp$",
|
||||
r".*\.swx$",
|
||||
r".*\.tmp$",
|
||||
r".*__pycache__.*",
|
||||
r".*\.pytest_cache.*",
|
||||
r".*/\.idea/.*",
|
||||
r".*/\.vscode/.*",
|
||||
r".*\.DS_Store$",
|
||||
r"#.*#$",
|
||||
]
|
||||
_watch_extensions = (".py", ".json", ".yaml", ".yml", ".toml")
|
||||
|
||||
def __init__(self, on_save, debounce_seconds: float = 0.5):
|
||||
super().__init__()
|
||||
self._on_save = on_save
|
||||
self._debounce = debounce_seconds
|
||||
self._last_triggered = 0
|
||||
self._last_file = None
|
||||
|
||||
def _should_ignore(self, path: str) -> bool:
|
||||
for pattern in self._ignore_patterns:
|
||||
if re.match(pattern, path):
|
||||
return True
|
||||
return not path.endswith(self._watch_extensions)
|
||||
|
||||
def _debounced_trigger(self, path: str):
|
||||
now = time.time()
|
||||
if path != self._last_file or now - self._last_triggered > self._debounce:
|
||||
self._last_triggered = now
|
||||
self._last_file = path
|
||||
self._on_save(path)
|
||||
|
||||
def on_modified(self, event):
|
||||
if not event.is_directory and isinstance(event, FileModifiedEvent):
|
||||
if not self._should_ignore(event.src_path):
|
||||
self._debounced_trigger(event.src_path)
|
||||
|
||||
def on_closed(self, event):
|
||||
if not event.is_directory and isinstance(event, FileClosedEvent):
|
||||
if not self._should_ignore(event.src_path):
|
||||
self._debounced_trigger(event.src_path)
|
||||
103
src/cli/cpl/cli/utils/live_server/live_server.py
Normal file
103
src/cli/cpl/cli/utils/live_server/live_server.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from cpl.cli.model.project import Project
|
||||
from cpl.cli.utils.live_server.file_event_handler import FileEventHandler
|
||||
from cpl.cli.utils.venv import get_venv_python, ensure_venv
|
||||
from cpl.core.configuration import Configuration
|
||||
from cpl.core.console import Console
|
||||
|
||||
|
||||
class LiveServer:
|
||||
def __init__(self, project: Project, args: list[str], dev: bool = False, verbose: bool = False):
|
||||
path = str(Path(project.path).parent.resolve().absolute())
|
||||
executable = (Path(project.path).parent / Path(project.directory) / project.main).resolve().absolute()
|
||||
|
||||
self._dist_path = None
|
||||
if not dev:
|
||||
self._dist_path = Path(project.path).parent / "dist"
|
||||
|
||||
if Configuration.get("workspace") is not None:
|
||||
self._dist_path = Path(Configuration.get("workspace").path).parent / "dist"
|
||||
|
||||
self._dist_path = Path(self._dist_path).resolve().absolute()
|
||||
if verbose:
|
||||
Console.write_line(f"Creating dist folder at {self._dist_path}...")
|
||||
|
||||
os.makedirs(self._dist_path, exist_ok=True)
|
||||
project.do_build(self._dist_path, verbose)
|
||||
path = self._dist_path / project.name / project.directory
|
||||
main = project.main.replace(project.directory, "").lstrip("/\\")
|
||||
|
||||
executable = (path / main).resolve().absolute()
|
||||
|
||||
if not os.path.isfile(executable):
|
||||
Console.error(f"Executable {executable} not found.")
|
||||
return
|
||||
|
||||
self._project = project
|
||||
self._sources = (Path(project.path).parent / Path(project.directory)).resolve().absolute()
|
||||
self._executable = executable
|
||||
self._working_directory = Path(path)
|
||||
self._args = args
|
||||
self._is_dev = dev
|
||||
self._verbose = verbose
|
||||
|
||||
self._process = None
|
||||
self._observer = None
|
||||
self._python = str(get_venv_python(ensure_venv(Path("./"))).absolute())
|
||||
|
||||
def _run_executable(self):
|
||||
if self._process:
|
||||
self._process.terminate()
|
||||
self._process.wait()
|
||||
|
||||
self._process = subprocess.Popen(
|
||||
[self._python, str(self._executable), *self._args],
|
||||
cwd=self._working_directory,
|
||||
)
|
||||
|
||||
def _on_change(self, changed_file: str):
|
||||
if self._verbose:
|
||||
Console.write_line(f"Change detected: {changed_file}")
|
||||
|
||||
if self._is_dev and self._process:
|
||||
self._process.terminate()
|
||||
self._process.wait()
|
||||
|
||||
Console.write_line("Restart\n\n")
|
||||
time.sleep(0.5) # debounce to avoid copy temp files
|
||||
if not self._is_dev:
|
||||
self._project.do_build(self._dist_path, verbose=self._verbose, silent=True)
|
||||
|
||||
self._run_executable()
|
||||
|
||||
def start(self):
|
||||
handler = FileEventHandler(self._on_change)
|
||||
observer = Observer()
|
||||
observer.schedule(handler, str(self._sources), recursive=True)
|
||||
observer.start()
|
||||
self._observer = observer
|
||||
|
||||
Console.write_line("** CPL live development server is running **\n")
|
||||
Console.write_line(f"Watching {self._sources} ... (Ctrl+C to stop)")
|
||||
Console.write_line(f"Starting {self._executable} ...\n\n")
|
||||
self._run_executable()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt as e:
|
||||
time.sleep(1)
|
||||
Console.write_line("Stopping...")
|
||||
finally:
|
||||
if self._process:
|
||||
self._process.terminate()
|
||||
self._process.wait()
|
||||
observer.stop()
|
||||
observer.join()
|
||||
Console.close()
|
||||
@@ -26,7 +26,6 @@ def get_project_by_name_or_path(project: str) -> Project:
|
||||
if p.name == project:
|
||||
return Project.from_file(Path(p.path))
|
||||
|
||||
|
||||
if not path.is_dir() and not path.is_file():
|
||||
raise ValueError(f"Unknown project {project}")
|
||||
|
||||
@@ -37,12 +36,14 @@ def get_project_by_name_or_path(project: str) -> Project:
|
||||
|
||||
raise ValueError(f"Project '{project}' not found.")
|
||||
|
||||
|
||||
def create_pyproject_toml(project: Project, path: Path):
|
||||
pyproject_path = path / "pyproject.toml"
|
||||
if pyproject_path.exists():
|
||||
return
|
||||
|
||||
content = textwrap.dedent(f"""
|
||||
content = textwrap.dedent(
|
||||
f"""
|
||||
[build-system]
|
||||
requires = ["setuptools>=70.1.0", "wheel", "build"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
@@ -53,6 +54,7 @@ def create_pyproject_toml(project: Project, path: Path):
|
||||
authors = [{{name="{project.author or ''}"}}]
|
||||
license = "{project.license or ''}"
|
||||
dependencies = [{', '.join([f'"{dep}"' for dep in project.dependencies])}]
|
||||
""").lstrip()
|
||||
"""
|
||||
).lstrip()
|
||||
|
||||
pyproject_path.write_text(content)
|
||||
|
||||
@@ -28,7 +28,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}...")
|
||||
Console.write_line(f"Creating virtual environment at {venv_path.absolute()}...")
|
||||
venv.EnvBuilder(with_pip=True).create(venv_path)
|
||||
return venv_path
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
cpl-core
|
||||
click==8.3.0
|
||||
click==8.3.0
|
||||
watchdog==6.0.0
|
||||
Reference in New Issue
Block a user