Added cpl new
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 8s

This commit is contained in:
2025-10-18 17:48:49 +02:00
parent 8ebac05dd8
commit 6fa11d3737
30 changed files with 416 additions and 136 deletions

View File

@@ -2,7 +2,8 @@
"name": "cpl", "name": "cpl",
"projects": [ "projects": [
"src/cli/cpl.project.json", "src/cli/cpl.project.json",
"src/core/cpl.project.json" "src/core/cpl.project.json",
"test/cpl.project.json"
], ],
"defaultProject": "cpl-cli", "defaultProject": "cpl-cli",
"scripts": { "scripts": {

View File

@@ -14,7 +14,7 @@ TApp = TypeVar("TApp", bound=ApplicationABC)
class ApplicationBuilder(Generic[TApp]): class ApplicationBuilder(Generic[TApp]):
def __init__(self, app: Type[ApplicationABC]): def __init__(self, app: Type[TApp]):
assert app is not None, "app must not be None" assert app is not None, "app must not be None"
assert issubclass(app, ApplicationABC), "app must be an subclass of ApplicationABC or its subclass" assert issubclass(app, ApplicationABC), "app must be an subclass of ApplicationABC or its subclass"

View File

@@ -1,7 +1,9 @@
import asyncio import asyncio
from typing import Callable from typing import Callable
from cpl.dependency import get_provider from cpl.core.property import classproperty
from cpl.dependency.context import get_provider, use_root_provider
from cpl.dependency.service_collection import ServiceCollection
from cpl.dependency.hosted.startup_task import StartupTask from cpl.dependency.hosted.startup_task import StartupTask
@@ -9,6 +11,24 @@ class Host:
_loop: asyncio.AbstractEventLoop | None = None _loop: asyncio.AbstractEventLoop | None = None
_tasks: dict = {} _tasks: dict = {}
_service_collection: ServiceCollection | None = None
@classproperty
def services(cls) -> ServiceCollection:
if cls._service_collection is None:
cls._service_collection = ServiceCollection()
return cls._service_collection
@classmethod
def get_provider(cls):
provider = get_provider()
if provider is None:
provider = cls.services.build()
use_root_provider(provider)
return provider
@classmethod @classmethod
def get_loop(cls) -> asyncio.AbstractEventLoop: def get_loop(cls) -> asyncio.AbstractEventLoop:
if cls._loop is None: if cls._loop is None:
@@ -18,7 +38,7 @@ class Host:
@classmethod @classmethod
def run_start_tasks(cls): def run_start_tasks(cls):
provider = get_provider() provider = cls.get_provider()
tasks = provider.get_services(StartupTask) tasks = provider.get_services(StartupTask)
loop = cls.get_loop() loop = cls.get_loop()
@@ -30,7 +50,7 @@ class Host:
@classmethod @classmethod
def run_hosted_services(cls): def run_hosted_services(cls):
provider = get_provider() provider = cls.get_provider()
services = provider.get_hosted_services() services = provider.get_hosted_services()
loop = cls.get_loop() loop = cls.get_loop()
@@ -49,6 +69,10 @@ class Host:
cls._tasks.clear() cls._tasks.clear()
@classmethod
async def wait_for_all(cls):
await asyncio.gather(*cls._tasks.values())
@classmethod @classmethod
def run_app(cls, func: Callable, *args, **kwargs): def run_app(cls, func: Callable, *args, **kwargs):
cls.run_start_tasks() cls.run_start_tasks()

View File

@@ -0,0 +1,7 @@
from cpl.core.console import Console
def main():
Console.write_line("Hello, World!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,46 @@
from cpl.api import ApiModule
from cpl.application import ApplicationBuilder
from cpl.core.configuration import Configuration
from cpl.graphql.application import GraphQLApp
from starlette.responses import JSONResponse
def main():
builder = ApplicationBuilder[GraphQLApp](GraphQLApp)
Configuration.add_json_file(f"appsettings.json", optional=True)
(
builder.services.add_logging()
# uncomment to add preferred database module
# .add_module(MySQLModule)
# .add_module(PostgresModule)
.add_module(ApiModule)
)
app = builder.build()
app.with_logging()
app.with_authentication()
app.with_authorization()
app.with_route(
path="/ping",
fn=lambda r: JSONResponse("pong"),
method="GET",
)
schema = app.with_graphql()
schema.query.string_field("ping", resolver=lambda: "pong")
app.with_auth_root_queries(True)
app.with_auth_root_mutations(True)
app.with_playground()
app.with_graphiql()
app.run()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,3 @@
class Class1:
def __init__(self): ...

View File

@@ -0,0 +1,13 @@
from cpl.application import Host
from my_hosted_service import MyHostedService
async def main():
Host.services.add_hosted_service(MyHostedService)
Host.run_start_tasks()
Host.run_hosted_services()
await Host.wait_for_all()
if __name__ == "__main__":
Host.run(main)

View File

@@ -0,0 +1,13 @@
from cpl.core.console import Console
from cpl.dependency.hosted import HostedService
class MyHostedService(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

@@ -0,0 +1,37 @@
from starlette.responses import JSONResponse
from cpl.api import ApiModule
from cpl.api.application import WebApp
from cpl.application import ApplicationBuilder
from cpl.core.configuration import Configuration
def main():
builder = ApplicationBuilder[WebApp](WebApp)
Configuration.add_json_file(f"appsettings.json", optional=True)
(
builder.services.add_logging()
# uncomment to add preferred database module
# .add_module(MySQLModule)
# .add_module(PostgresModule)
.add_module(ApiModule)
)
app = builder.build()
app.with_logging()
app.with_authentication()
app.with_authorization()
app.with_route(
path="/ping",
fn=lambda r: JSONResponse("pong"),
method="GET",
)
app.run()
if __name__ == "__main__":
main()

View File

@@ -3,7 +3,7 @@ from pathlib import Path
import click import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.cli.utils.structure import Structure
from cpl.core.console import Console from cpl.core.console import Console
@@ -12,8 +12,8 @@ from cpl.core.console import Console
@click.argument("target", type=click.STRING, required=True) @click.argument("target", type=click.STRING, required=True)
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def add(reference: str, target: str, verbose: bool): def add(reference: str, target: str, verbose: bool):
reference_project = get_project_by_name_or_path(reference) reference_project = Structure.get_project_by_name_or_path(reference)
target_project = get_project_by_name_or_path(target) target_project = Structure.get_project_by_name_or_path(target)
if reference_project.name == target_project.name: if reference_project.name == target_project.name:
raise ValueError("Cannot add a project as a dependency to itself!") raise ValueError("Cannot add a project as a dependency to itself!")

View File

@@ -4,7 +4,7 @@ import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.utils.pip import Pip from cpl.cli.utils.pip import Pip
from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.cli.utils.structure import Structure
from cpl.core.console import Console from cpl.core.console import Console
@@ -14,7 +14,7 @@ from cpl.core.console import Console
@click.option("--dev", is_flag=True, help="Include dev dependencies") @click.option("--dev", is_flag=True, help="Include dev dependencies")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def install(package: str, project: str, dev: bool, verbose: bool): def install(package: str, project: str, dev: bool, verbose: bool):
project = get_project_by_name_or_path(project or "./") project = Structure.get_project_by_name_or_path(project or "./")
if package is not None: if package is not None:
Console.write_line(f"Installing {package} to '{project.name}':") Console.write_line(f"Installing {package} to '{project.name}':")

View File

@@ -3,7 +3,7 @@ from pathlib import Path
import click import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.cli.utils.structure import Structure
from cpl.core.console import Console from cpl.core.console import Console
@@ -11,8 +11,8 @@ from cpl.core.console import Console
@click.argument("reference", type=click.STRING, required=True) @click.argument("reference", type=click.STRING, required=True)
@click.argument("target", type=click.STRING, required=True) @click.argument("target", type=click.STRING, required=True)
def remove(reference: str, target: str): def remove(reference: str, target: str):
reference_project = get_project_by_name_or_path(reference) reference_project = Structure.get_project_by_name_or_path(reference)
target_project = get_project_by_name_or_path(target) target_project = Structure.get_project_by_name_or_path(target)
if reference_project.name == target_project.name: if reference_project.name == target_project.name:
raise ValueError("Cannot add a project as a dependency to itself!") raise ValueError("Cannot add a project as a dependency to itself!")

View File

@@ -4,7 +4,7 @@ import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.utils.pip import Pip from cpl.cli.utils.pip import Pip
from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.cli.utils.structure import Structure
from cpl.core.console import Console from cpl.core.console import Console
@@ -17,7 +17,7 @@ def uninstall(package: str, project: str, dev: bool, verbose: bool):
if package is None: if package is None:
package = Console.read("Package name to uninstall: ").strip() package = Console.read("Package name to uninstall: ").strip()
project = get_project_by_name_or_path(project or "./") project = Structure.get_project_by_name_or_path(project or "./")
deps = project.dependencies if not dev else project.dev_dependencies deps = project.dependencies if not dev else project.dev_dependencies

View File

@@ -4,7 +4,7 @@ import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.utils.pip import Pip from cpl.cli.utils.pip import Pip
from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.cli.utils.structure import Structure
from cpl.core.console import Console from cpl.core.console import Console
@@ -14,7 +14,7 @@ from cpl.core.console import Console
@click.option("--dev", is_flag=True, help="Include dev dependencies") @click.option("--dev", is_flag=True, help="Include dev dependencies")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def update(package: str, project: str, dev: bool, verbose: bool): def update(package: str, project: str, dev: bool, verbose: bool):
project = get_project_by_name_or_path(project or "./") project = Structure.get_project_by_name_or_path(project or "./")
deps: dict = project.dependencies deps: dict = project.dependencies
if dev: if dev:

View File

@@ -5,8 +5,6 @@ from pathlib import Path
import click import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.model.project import Project
from cpl.cli.utils.structure import get_project_by_name_or_path
from cpl.cli.utils.venv import ensure_venv, get_venv_python from cpl.cli.utils.venv import ensure_venv, get_venv_python
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.console import Console from cpl.core.console import Console
@@ -18,7 +16,9 @@ from cpl.core.console import Console
@click.option("--skip-py-build", "-spb", is_flag=True, help="Skip toml generation and python build") @click.option("--skip-py-build", "-spb", is_flag=True, help="Skip toml generation and python build")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def build(project: str, dist: str = None, skip_py_build: bool = None, verbose: bool = None): def build(project: str, dist: str = None, skip_py_build: bool = None, verbose: bool = None):
project = get_project_by_name_or_path(project or "./") from cpl.cli.utils.structure import Structure
project = Structure.get_project_by_name_or_path(project or "./")
venv = ensure_venv().absolute() venv = ensure_venv().absolute()
dist_path = dist or Path(project.path).parent / "dist" dist_path = dist or Path(project.path).parent / "dist"
@@ -38,9 +38,7 @@ def build(project: str, dist: str = None, skip_py_build: bool = None, verbose: b
Console.write_line("\nDone!") Console.write_line("\nDone!")
return return
from cpl.cli.utils.structure import create_pyproject_toml Structure.create_pyproject_toml(project, dist_path / project.name)
create_pyproject_toml(project, dist_path / project.name)
python = str(get_venv_python(venv)) python = str(get_venv_python(venv))
result = Console.spinner( result = Console.spinner(

View File

@@ -5,19 +5,20 @@ from pathlib import Path
import click import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.cli.utils.structure import Structure
from cpl.cli.utils.venv import get_venv_python, ensure_venv from cpl.cli.utils.venv import get_venv_python, ensure_venv
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.console import Console from cpl.core.console import Console
@cli.command("run", aliases=["r"]) @cli.command("run", aliases=["r"])
@click.argument("project", type=str)
@click.argument("args", nargs=-1) @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("--dev", "-d", is_flag=True, help="Use sources instead of build output")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def run(project: str, args: list[str], dev: bool, verbose: bool): def run(project: str, args: list[str], dev: bool, verbose: bool):
project = get_project_by_name_or_path(project or "./") Console.error(project)
project = Structure.get_project_by_name_or_path(project or "./")
if project.main is None: if project.main is None:
Console.error(f"Project {project.name} has no executable") Console.error(f"Project {project.name} has no executable")
return return

View File

@@ -2,17 +2,17 @@ import click
from cpl.cli.cli import cli from cpl.cli.cli import cli
from cpl.cli.utils.live_server.live_server import LiveServer from cpl.cli.utils.live_server.live_server import LiveServer
from cpl.cli.utils.structure import get_project_by_name_or_path from cpl.cli.utils.structure import Structure
from cpl.core.console import Console from cpl.core.console import Console
@cli.command("start", aliases=["s"]) @cli.command("start", aliases=["s"])
@click.argument("project", type=str)
@click.argument("args", nargs=-1) @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("--dev", "-d", is_flag=True, help="Use sources instead of build output")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
def start(project: str, args: list[str], dev: bool, verbose: bool): def start(project: str, args: list[str], dev: bool, verbose: bool):
project = get_project_by_name_or_path(project or "./") project = Structure.get_project_by_name_or_path(project or "./")
if project.main is None: if project.main is None:
Console.error(f"Project {project.name} has no executable") Console.error(f"Project {project.name} has no executable")
return return

View File

@@ -7,7 +7,7 @@ from cpl.cli import cli as clim
from cpl.cli.cli import cli from cpl.cli.cli 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.structure import get_project_by_name_or_path from cpl.cli.utils.structure import Structure
from cpl.cli.utils.template_collector import TemplateCollector from cpl.cli.utils.template_collector import TemplateCollector
from cpl.cli.utils.template_renderer import TemplateRenderer from cpl.cli.utils.template_renderer import TemplateRenderer
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
@@ -29,7 +29,7 @@ def generate(schematic: str, name: str, verbose: bool) -> None:
project = None project = None
try: try:
project = get_project_by_name_or_path("./") project = Structure.get_project_by_name_or_path("./")
if verbose: if verbose:
Console.write_line("project found, collecting templates...") Console.write_line("project found, collecting templates...")

View File

@@ -3,9 +3,8 @@ 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.model.project import Project
from cpl.cli.model.workspace import Workspace
from cpl.cli.utils.prompt import SmartChoice from cpl.cli.utils.prompt import SmartChoice
from cpl.cli.utils.structure import Structure
from cpl.core.console import Console from cpl.core.console import Console
@@ -25,62 +24,10 @@ 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"]:
_init_workspace(name or click.prompt("Workspace name", default="my-workspace")) Structure.init_workspace("./", name or click.prompt("Workspace name", default="my-workspace"))
elif target in PROJECT_TYPES: elif target in PROJECT_TYPES:
_init_project(name or click.prompt("Project name", default=f"my-{target}"), target) workspace = Structure.find_workspace_in_path(Path(name).parent)
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)
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,64 @@
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.structure import Structure
from cpl.core.console import Console
@cli.command("new", aliases=["n"])
@click.argument("type", 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(
"--project",
"-p",
nargs=2,
metavar="<type> <name>",
help="Optional: when creating a workspace, also create a project with the given name and type.",
)
@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
if len(project) == 2:
type = project[0]
if type not in PROJECT_TYPES + PROJECT_TYPES_SHORT:
raise ValueError(f"Unknown project type '{type}'")
path = Path(workspace.path).parent / Path(project[1]).parent
project_name = Path(project[1]).stem
workspace = Structure.find_workspace_in_path(path)
if workspace is None:
Console.error("No workspace found. Please run 'cpl init workspace' first.")
raise SystemExit(1)
if project_name in workspace.project_names:
Console.error(f"Project '{project_name}' already exists in the workspace")
raise SystemExit(1)
if verbose:
Console.write_line(f"Creating project '{path/project_name}'...")
project_types = os.listdir(Path(clim.__file__).parent / ".cpl" / "new")
project_types.extend(set(x[0] for x in PROJECT_TYPES))
if type not in project_types:
raise ValueError(f"Unsupported project type '{type}'")
Structure.create_project(path, type, project_name, workspace, verbose)
if workspace_created:
workspace.default_project = project_name
workspace.save()

View File

@@ -12,6 +12,7 @@ from cpl.cli.command.project.run import run
from cpl.cli.command.project.start import start from cpl.cli.command.project.start import start
from cpl.cli.command.structure.generate import generate from cpl.cli.command.structure.generate import generate
from cpl.cli.command.structure.init import init from cpl.cli.command.structure.init import init
from cpl.cli.command.structure.new import new
from cpl.cli.command.version import version 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
@@ -36,7 +37,7 @@ def _load_scripts():
if ws is None: if ws is None:
continue continue
Configuration.set("workspace", Workspace.from_file(p)) Configuration.set("workspace", ws)
return ws.scripts return ws.scripts
return {} return {}
@@ -54,7 +55,7 @@ def configure():
# structure # structure
cli.add_command(init) cli.add_command(init)
# cli.add_command(new) cli.add_command(new)
cli.add_command(generate) cli.add_command(generate)
# packaging # packaging

View File

@@ -3,6 +3,10 @@ from cpl.cli.model.cpl_sub_structure_model import CPLSubStructureModel
class Build(CPLSubStructureModel): class Build(CPLSubStructureModel):
@staticmethod
def new(include: list[str], exclude: list[str]) -> "Build":
return Build(include, exclude)
def __init__(self, include: list[str], exclude: list[str]): def __init__(self, include: list[str], exclude: list[str]):
CPLSubStructureModel.__init__(self) CPLSubStructureModel.__init__(self)

View File

@@ -1,5 +1,6 @@
import inspect import inspect
import json import json
import os
from inspect import isclass from inspect import isclass
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Type, TypeVar from typing import Any, Dict, List, Optional, Type, TypeVar
@@ -81,6 +82,9 @@ class CPLStructureModel:
if not self._path: if not self._path:
raise ValueError("Cannot save model without a path.") raise ValueError("Cannot save model without a path.")
if not Path(self._path).exists():
os.makedirs(Path(self._path).parent, exist_ok=True)
with open(self._path, "w", encoding="utf-8") as f: with open(self._path, "w", encoding="utf-8") as f:
json.dump(self.to_json(), f, indent=2) json.dump(self.to_json(), f, indent=2)

View File

@@ -31,7 +31,8 @@ class Project(CPLStructureModel):
{}, {},
[], [],
None, None,
"src", "./",
Build.new([], []),
) )
def __init__( def __init__(
@@ -192,6 +193,7 @@ class Project(CPLStructureModel):
def _collect_files(self, rel_dir: Path) -> List[Path]: def _collect_files(self, rel_dir: Path) -> List[Path]:
files: List[Path] = [] files: List[Path] = []
exclude_patterns = [p.strip() for p in self._build.exclude or []] exclude_patterns = [p.strip() for p in self._build.exclude or []]
exclude_patterns.append("cpl.*.json")
for root, dirnames, filenames in os.walk(rel_dir, topdown=True): for root, dirnames, filenames in os.walk(rel_dir, topdown=True):
root_path = Path(root) root_path = Path(root)

View File

@@ -1,42 +1,30 @@
import os
import shutil
import textwrap import textwrap
from pathlib import Path from pathlib import Path
import click
import cpl.cli as cli
from cpl.cli.model.project import Project from cpl.cli.model.project import Project
from cpl.core.configuration import Configuration from cpl.cli.model.workspace import Workspace
from cpl.cli.utils.template_renderer import TemplateRenderer
from cpl.core.console import Console
def get_project_by_name_or_path(project: str) -> Project: class Structure:
if project is None: @staticmethod
raise ValueError("Project name or path must be provided.") def find_workspace_in_path(path: Path) -> Workspace | None:
current_path = path.resolve()
workspace = Configuration.get("workspace") for parent in [current_path] + list(current_path.parents):
workspace_file = parent / "cpl.workspace.json"
path = Path(project) if workspace_file.exists() and workspace_file.is_file():
if path.exists() and path.is_dir() and (path / "cpl.project.json").exists(): return Workspace.from_file(workspace_file)
return Project.from_file(path / "cpl.project.json")
if path.exists() and path.is_file():
if not path.name.endswith("cpl.project.json"):
raise ValueError(f"File '{path}' is not a valid cpl.project.json file.")
return Project.from_file(path)
if workspace is not None:
for p in workspace.actual_projects:
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}")
if workspace.default_project is not None:
for p in workspace.actual_projects:
if p.name == workspace.default_project:
return Project.from_file(Path(p.path))
raise ValueError(f"Project '{project}' not found.")
return None
@staticmethod
def create_pyproject_toml(project: Project, path: Path): def create_pyproject_toml(project: Project, path: Path):
pyproject_path = path / "pyproject.toml" pyproject_path = path / "pyproject.toml"
if pyproject_path.exists(): if pyproject_path.exists():
@@ -58,3 +46,125 @@ def create_pyproject_toml(project: Project, path: Path):
).lstrip() ).lstrip()
pyproject_path.write_text(content) pyproject_path.write_text(content)
@staticmethod
def get_project_by_name_or_path(project: str) -> Project:
if project is None:
raise ValueError("Project name or path must be provided.")
path = Path(project)
if path.exists() and path.is_dir() and (path / "cpl.project.json").exists():
return Project.from_file(path / "cpl.project.json")
if path.exists() and path.is_file():
if not path.name.endswith("cpl.project.json"):
raise ValueError(f"File '{path}' is not a valid cpl.project.json file.")
return Project.from_file(path)
workspace = Structure.find_workspace_in_path(path.parent)
if workspace is None:
raise RuntimeError("No workspace found. Please run 'cpl init workspace' first.")
if workspace is not None:
for p in workspace.actual_projects:
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}")
if workspace.default_project is not None:
for p in workspace.actual_projects:
if p.name == workspace.default_project:
return Project.from_file(Path(p.path))
raise ValueError(f"Project '{project}' not found.")
@staticmethod
def init_workspace(path: Path | str, name: str):
path = Path(path) / Path("cpl.workspace.json")
if path.exists():
raise ValueError("workspace.json already exists.")
workspace = Workspace.new(str(path), name)
workspace.save()
Console.write_line(f"Created workspace '{name}'")
@staticmethod
def init_project(rel_path: str, name: str, project_type: str, workspace: Workspace | None, verbose=False):
if not Path(rel_path).exists():
rel_path = click.prompt(
"Project directory", type=click.Path(exists=True, file_okay=False), default="src"
)
path = Path(rel_path) / Path("cpl.project.json")
if path.exists():
Console.write_line("cpl.project.json already exists.")
return
project = Project.new(str(path), name, project_type)
executable_path = Path(project.path).parent / "main.py"
executable_file = str(executable_path.relative_to(Path(project.path).parent)) if executable_path.exists() else None
if project_type in ["console", "web", "service"]:
project.main = executable_file or click.prompt(
"Main executable", type=click.Path(exists=True, dir_okay=False), default="src/main.py"
)
project.save()
if workspace is not None:
rel_path = str(path.resolve().absolute().relative_to(Path(workspace.path).parent))
if rel_path not in workspace.projects:
workspace.projects.append(rel_path)
workspace.save()
if verbose:
Console.write_line(f"Registered '{name}' in workspace.json")
Console.write_line(f"Created {project_type} project '{name}'")
@staticmethod
def create_project(path: Path, project_type: str, name: str, workspace: Workspace | None, verbose=False):
if not str(path).endswith(name):
path = path / name
if not path.exists():
os.makedirs(path, exist_ok=True)
src_dir = Path(cli.__file__).parent / ".cpl" / "new" / project_type
Console.write_line()
for root, dirs, files in os.walk(src_dir):
rel_root = Path(root).relative_to(src_dir)
target_root = path / rel_root
target_root.mkdir(parents=True, exist_ok=True)
for filename in files:
src_file = Path(root) / filename
tgt_file = target_root / filename
Console.set_foreground_color("green")
Console.write_line(f"Create {str(tgt_file).replace(".schematic", "")}")
Console.set_foreground_color()
if filename.endswith(".schematic"):
with open(src_file, "r") as src:
with open(str(tgt_file).replace(".schematic", ""), "w") as tgt:
tgt.write(
TemplateRenderer.render_template(
str(src_file).split(".")[0], src.read(), name, str(path)
)
)
continue
shutil.copy(src_file, tgt_file)
Console.write_line()
Structure.init_project(str(path), name, project_type, workspace)

View File

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

View File

@@ -133,5 +133,4 @@ class Configuration:
if isclass(key) and issubclass(key, ConfigurationModelABC) and result == default: if isclass(key) and issubclass(key, ConfigurationModelABC) and result == default:
result = key() result = key()
cls.set(key, result) cls.set(key, result)
return result return result

View File

@@ -72,13 +72,17 @@ class Console:
cls._background_color = color cls._background_color = color
@classmethod @classmethod
def set_foreground_color(cls, color: Union[ForegroundColorEnum, str]): def set_foreground_color(cls, color: Union[ForegroundColorEnum, str] = None):
r"""Sets the foreground color r"""Sets the foreground color
Parameter: Parameter:
color: Union[:class:`cpl.core.console.background_color_enum.BackgroundColorEnum`, :class:`str`] color: Union[:class:`cpl.core.console.background_color_enum.BackgroundColorEnum`, :class:`str`]
Foreground color of the console Foreground color of the console
""" """
if color is None:
cls._foreground_color = ForegroundColorEnum.default
return
if type(color) is str: if type(color) is str:
cls._foreground_color = ForegroundColorEnum[color] cls._foreground_color = ForegroundColorEnum[color]
else: else:

View File

@@ -0,0 +1,3 @@
class classproperty(property):
def __get__(self, obj, cls):
return self.fget(cls)

View File

@@ -1 +1 @@
from .graphql_app import WebApp from .graphql_app import GraphQLApp