WIP: dev into master #184
@@ -13,7 +13,19 @@
|
||||
"devDependencies": {
|
||||
"black": "~25.9"
|
||||
},
|
||||
"references": [],
|
||||
"references": [
|
||||
"../core/cpl.project.json"
|
||||
],
|
||||
"main": "cpl/cli/main.py",
|
||||
"directory": "cpl/cli"
|
||||
"directory": "cpl/cli",
|
||||
"build": {
|
||||
"include": [
|
||||
"_templates/"
|
||||
],
|
||||
"exclude": [
|
||||
"**/__pycache__",
|
||||
"**/logs",
|
||||
"**/tests"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from cpl.cli.cli import cli
|
||||
@@ -8,7 +10,8 @@ from cpl.core.console import Console
|
||||
@cli.command("add", aliases=["a"])
|
||||
@click.argument("reference", type=click.STRING, required=True)
|
||||
@click.argument("target", type=click.STRING, required=True)
|
||||
def add(reference: str, target: str):
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
||||
def add(reference: str, target: str, verbose: bool):
|
||||
reference_project = get_project_by_name_or_path(reference)
|
||||
target_project = get_project_by_name_or_path(target)
|
||||
|
||||
@@ -17,6 +20,8 @@ def add(reference: str, target: str):
|
||||
|
||||
if reference_project.path in target_project.references:
|
||||
raise ValueError(f"Project '{reference_project.name}' is already a reference of '{target_project.name}'")
|
||||
target_project.references.append(reference_project.path)
|
||||
|
||||
rel_path = Path(reference_project.path).relative_to(Path(target_project.path).parent, walk_up=True)
|
||||
target_project.references.append(str(rel_path))
|
||||
target_project.save()
|
||||
Console.write_line(f"Added '{reference_project.name}' to '{target_project.name}' project")
|
||||
|
||||
@@ -12,7 +12,7 @@ from cpl.core.console import Console
|
||||
@click.argument("package", type=click.STRING, required=False)
|
||||
@click.argument("project", type=click.STRING, required=False)
|
||||
@click.option("--dev", is_flag=True, help="Include dev dependencies")
|
||||
@click.option("--verbose", 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):
|
||||
project = get_project_by_name_or_path(project or "./")
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from cpl.cli.cli import cli
|
||||
@@ -15,9 +17,10 @@ def remove(reference: str, target: str):
|
||||
if reference_project.name == target_project.name:
|
||||
raise ValueError("Cannot add a project as a dependency to itself!")
|
||||
|
||||
if reference_project.path not in target_project.references:
|
||||
rel_path = str(Path(reference_project.path).relative_to(Path(target_project.path).parent, walk_up=True))
|
||||
if rel_path not in target_project.references:
|
||||
raise ValueError(f"Project '{reference_project.name}' isn't a reference of '{target_project.name}'")
|
||||
|
||||
target_project.references.remove(reference_project.path)
|
||||
target_project.references.remove(rel_path)
|
||||
target_project.save()
|
||||
Console.write_line(f"Removed '{reference_project.name}' from '{target_project.name}' project")
|
||||
|
||||
@@ -12,7 +12,7 @@ from cpl.core.console import Console
|
||||
@click.argument("package", required=False)
|
||||
@click.argument("project", type=click.STRING, required=False)
|
||||
@click.option("--dev", is_flag=True, help="Include dev dependencies")
|
||||
@click.option("--verbose", is_flag=True, help="Enable verbose output")
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
||||
def uninstall(package: str, project: str, dev: bool, verbose: bool):
|
||||
if package is None:
|
||||
package = Console.read("Package name to uninstall: ").strip()
|
||||
|
||||
@@ -12,7 +12,7 @@ from cpl.core.console import Console
|
||||
@click.argument("package", type=click.STRING, required=False)
|
||||
@click.argument("project", type=click.STRING, required=False)
|
||||
@click.option("--dev", is_flag=True, help="Include dev dependencies")
|
||||
@click.option("--verbose", 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):
|
||||
project = get_project_by_name_or_path(project or "./")
|
||||
|
||||
|
||||
31
src/cli/cpl/cli/command/project/build.py
Normal file
31
src/cli/cpl/cli/command/project/build.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os.path
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from cpl.cli.cli import cli
|
||||
from cpl.cli.utils.structure import get_project_by_name_or_path
|
||||
from cpl.core.configuration import Configuration
|
||||
from cpl.core.console import Console
|
||||
|
||||
|
||||
@cli.command("build", aliases=["b"])
|
||||
@click.argument("project", type=click.STRING, required=False)
|
||||
@click.option("--dist", "-d", type=str)
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
||||
def build(project: str, dist: str | None, verbose: bool):
|
||||
project = get_project_by_name_or_path(project or "./")
|
||||
dist_path = dist or Path(project.path).parent / "dist"
|
||||
|
||||
if dist is None and Configuration.get("workspace") is not None:
|
||||
dist_path = Path(Configuration.get("workspace").path).parent / "dist"
|
||||
|
||||
dist_path = Path(dist_path).resolve().absolute()
|
||||
|
||||
if verbose:
|
||||
Console.write_line(f"Creating dist folder at {dist_path}...")
|
||||
|
||||
os.makedirs(dist_path, exist_ok=True)
|
||||
|
||||
project.do_build(dist_path, verbose)
|
||||
Console.write_line("\nDone!")
|
||||
@@ -1,14 +1,14 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from cpl.cli.cli import cli
|
||||
from cpl.cli.command.execute.run import run
|
||||
from cpl.cli.command.package.remove import remove
|
||||
from cpl.cli.command.package.add import add
|
||||
from cpl.cli.command.structure.init import init
|
||||
from cpl.cli.command.package.install import install
|
||||
from cpl.cli.command.package.remove import remove
|
||||
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.structure.init import init
|
||||
from cpl.cli.command.version import version
|
||||
from cpl.cli.model.workspace import Workspace
|
||||
from cpl.cli.utils.custom_command import script_command
|
||||
@@ -61,6 +61,7 @@ def configure():
|
||||
cli.add_command(remove)
|
||||
|
||||
# run
|
||||
cli.add_command(build)
|
||||
cli.add_command(run)
|
||||
# cli.add_command(start)
|
||||
|
||||
|
||||
18
src/cli/cpl/cli/model/build.py
Normal file
18
src/cli/cpl/cli/model/build.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from cpl.cli.model.cpl_sub_structure_model import CPLSubStructureModel
|
||||
|
||||
|
||||
class Build(CPLSubStructureModel):
|
||||
|
||||
def __init__(self, include: list[str], exclude: list[str]):
|
||||
CPLSubStructureModel.__init__(self)
|
||||
|
||||
self._include = include
|
||||
self._exclude = exclude
|
||||
|
||||
@property
|
||||
def include(self) -> list[str]:
|
||||
return self._include
|
||||
|
||||
@property
|
||||
def exclude(self) -> list[str]:
|
||||
return self._exclude
|
||||
@@ -1,8 +1,11 @@
|
||||
import inspect
|
||||
import json
|
||||
from inspect import isclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Type, TypeVar
|
||||
|
||||
from cpl.cli.model.cpl_sub_structure_model import CPLSubStructureModel
|
||||
|
||||
T = TypeVar("T", bound="CPLStructureModel")
|
||||
|
||||
|
||||
@@ -35,6 +38,10 @@ class CPLStructureModel:
|
||||
kwargs[name] = str(path)
|
||||
continue
|
||||
|
||||
if isclass(param.annotation) and issubclass(param.annotation, CPLSubStructureModel):
|
||||
kwargs[name] = param.annotation.from_json(data[name])
|
||||
continue
|
||||
|
||||
if name in data:
|
||||
kwargs[name] = data[name]
|
||||
continue
|
||||
@@ -63,6 +70,10 @@ class CPLStructureModel:
|
||||
if not key.startswith("_") or key == "_path":
|
||||
continue
|
||||
out_key = _self_or_cls_snake_to_camel(key[1:])
|
||||
|
||||
if isinstance(value, CPLSubStructureModel):
|
||||
value = value.to_json()
|
||||
|
||||
result[out_key] = value
|
||||
return result
|
||||
|
||||
|
||||
104
src/cli/cpl/cli/model/cpl_sub_structure_model.py
Normal file
104
src/cli/cpl/cli/model/cpl_sub_structure_model.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import inspect
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Type, TypeVar
|
||||
|
||||
T = TypeVar("T", bound="CPLSubStructureModel")
|
||||
|
||||
|
||||
class CPLSubStructureModel:
|
||||
def __init__(self, path: Optional[str] = None):
|
||||
self._path = path
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[T], data: Dict[str, Any]) -> T:
|
||||
sig = inspect.signature(cls.__init__)
|
||||
kwargs: Dict[str, Any] = {}
|
||||
for name, param in list(sig.parameters.items())[1:]:
|
||||
if name in data:
|
||||
kwargs[name] = data[name]
|
||||
continue
|
||||
|
||||
priv = "_" + name
|
||||
if priv in data:
|
||||
kwargs[name] = data[priv]
|
||||
continue
|
||||
|
||||
camel = _self_or_cls_snake_to_camel(name)
|
||||
if camel in data:
|
||||
kwargs[name] = data[camel]
|
||||
continue
|
||||
|
||||
if param.default is not inspect._empty:
|
||||
kwargs[name] = param.default
|
||||
continue
|
||||
|
||||
raise KeyError(f"Missing required field '{name}' for {cls.__name__}.")
|
||||
|
||||
return cls(**kwargs)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
result: Dict[str, Any] = {}
|
||||
for key, value in self.__dict__.items():
|
||||
if not key.startswith("_") or key == "_path":
|
||||
continue
|
||||
out_key = _self_or_cls_snake_to_camel(key[1:])
|
||||
result[out_key] = value
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _require_str(value, field: str, allow_empty: bool = True) -> str:
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"{field} must be of type str")
|
||||
if not allow_empty and not value.strip():
|
||||
raise ValueError(f"{field} must not be empty")
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _require_optional_non_empty_str(value, field: str) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"{field} must be str or None")
|
||||
s = value.strip()
|
||||
if not s:
|
||||
raise ValueError(f"{field} must not be empty when set")
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def _require_list_of_str(value, field: str) -> List[str]:
|
||||
if not isinstance(value, list):
|
||||
raise TypeError(f"{field} must be a list")
|
||||
out: List[str] = []
|
||||
for i, v in enumerate(value):
|
||||
if not isinstance(v, str):
|
||||
raise TypeError(f"{field}[{i}] must be of type str")
|
||||
s = v.strip()
|
||||
if s:
|
||||
out.append(s)
|
||||
|
||||
seen = set()
|
||||
uniq: List[str] = []
|
||||
for s in out:
|
||||
if s not in seen:
|
||||
seen.add(s)
|
||||
uniq.append(s)
|
||||
return uniq
|
||||
|
||||
@staticmethod
|
||||
def _require_dict_str_str(value, field: str) -> Dict[str, str]:
|
||||
if not isinstance(value, dict):
|
||||
raise TypeError(f"{field} must be a dict")
|
||||
out: Dict[str, str] = {}
|
||||
for k, v in value.items():
|
||||
if not isinstance(k, str) or not k.strip():
|
||||
raise TypeError(f"Keys in {field} must be non-empty strings")
|
||||
if not isinstance(v, str) or not v.strip():
|
||||
raise TypeError(f"Values in {field} must be non-empty strings")
|
||||
out[k.strip()] = v.strip()
|
||||
return out
|
||||
|
||||
|
||||
def _self_or_cls_snake_to_camel(s: str) -> str:
|
||||
parts = s.split("_")
|
||||
return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:])
|
||||
@@ -1,8 +1,14 @@
|
||||
import fnmatch
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from cpl.cli.model.build import Build
|
||||
from cpl.cli.model.cpl_structure_model import CPLStructureModel
|
||||
from cpl.core.console import Console
|
||||
|
||||
|
||||
class Project(CPLStructureModel):
|
||||
@@ -44,6 +50,7 @@ class Project(CPLStructureModel):
|
||||
references: List[str],
|
||||
main: Optional[str],
|
||||
directory: str,
|
||||
build: Build,
|
||||
):
|
||||
CPLStructureModel.__init__(self, path)
|
||||
|
||||
@@ -60,6 +67,7 @@ class Project(CPLStructureModel):
|
||||
self._references = references
|
||||
self._main = main
|
||||
self._directory = directory
|
||||
self._build = build
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -176,3 +184,86 @@ class Project(CPLStructureModel):
|
||||
@directory.setter
|
||||
def directory(self, value: str):
|
||||
self._directory = self._require_str(value, "directory", allow_empty=False).strip()
|
||||
|
||||
@property
|
||||
def build(self) -> Build:
|
||||
return self._build
|
||||
|
||||
def _collect_files(self, rel_dir: Path) -> List[Path]:
|
||||
files: List[Path] = []
|
||||
exclude_patterns = [p.strip() for p in self._build.exclude or []]
|
||||
|
||||
for root, dirnames, filenames in os.walk(rel_dir, topdown=True):
|
||||
root_path = Path(root)
|
||||
rel_root = root_path.relative_to(rel_dir).as_posix()
|
||||
|
||||
dirnames[:] = [
|
||||
d
|
||||
for d in dirnames
|
||||
if not any(
|
||||
fnmatch.fnmatch(f"{rel_root}/{d}" if rel_root else d, pattern) or fnmatch.fnmatch(d, pattern)
|
||||
for pattern in exclude_patterns
|
||||
)
|
||||
]
|
||||
|
||||
for filename in filenames:
|
||||
rel_path = f"{rel_root}/{filename}" if rel_root else filename
|
||||
if any(
|
||||
fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(filename, pattern)
|
||||
for pattern in exclude_patterns
|
||||
):
|
||||
continue
|
||||
|
||||
files.append(root_path / filename)
|
||||
|
||||
return files
|
||||
|
||||
def build_references(self, dist: Path, verbose: bool = False):
|
||||
references = []
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(Path(self.path).parent)
|
||||
for ref in self.references:
|
||||
os.chdir(Path(ref).parent)
|
||||
references.append(Project.from_file(ref))
|
||||
|
||||
for p in references:
|
||||
os.chdir(Path(p.path).parent)
|
||||
p.do_build(dist, verbose)
|
||||
|
||||
os.chdir(old_dir)
|
||||
|
||||
def do_build(self, dist: Path, verbose: bool = False):
|
||||
self.build_references(dist, verbose)
|
||||
|
||||
Console.write_line(f"Building project {self.name}...")
|
||||
if isinstance(dist, str):
|
||||
dist = Path(dist)
|
||||
|
||||
dist_path = dist / self.name
|
||||
rel_dir = Path(self.path).parent / Path(self.directory)
|
||||
|
||||
if verbose:
|
||||
Console.write_line(f" Collecting project '{self.name}' files...")
|
||||
|
||||
files = self._collect_files(rel_dir)
|
||||
|
||||
if len(files) == 0:
|
||||
return
|
||||
|
||||
if verbose:
|
||||
Console.write_line(f" Cleaning dist folder at {dist_path.absolute()}...")
|
||||
|
||||
shutil.rmtree(str(dist_path), ignore_errors=True)
|
||||
|
||||
for file in files:
|
||||
rel_path = file.relative_to(rel_dir)
|
||||
dest_file_path = dist_path / rel_path
|
||||
|
||||
if not dest_file_path.parent.exists():
|
||||
os.makedirs(dest_file_path.parent, exist_ok=True)
|
||||
|
||||
shutil.copy(file, dest_file_path)
|
||||
|
||||
if verbose:
|
||||
Console.write_line(f" Copied {len(files)} files from {rel_dir.absolute()} to {dist_path.absolute()}")
|
||||
Console.write_line(" Done!")
|
||||
|
||||
@@ -25,6 +25,10 @@ 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}")
|
||||
|
||||
if workspace.default_project is not None:
|
||||
for p in workspace.actual_projects:
|
||||
if p.name == workspace.default_project:
|
||||
|
||||
@@ -8,6 +8,5 @@ export PYTHONPATH="$ROOT_DIR/core:$ROOT_DIR/cli:$PYTHONPATH"
|
||||
|
||||
old_dir="$(pwd)"
|
||||
cd ../
|
||||
echo "$@"
|
||||
python -m cpl.cli.main "$@"
|
||||
cd "$old_dir"
|
||||
@@ -20,5 +20,13 @@
|
||||
},
|
||||
"references": [],
|
||||
"main": null,
|
||||
"directory": "cpl/core"
|
||||
"directory": "cpl/core",
|
||||
"build": {
|
||||
"include": [],
|
||||
"exclude": [
|
||||
"**/__pycache__",
|
||||
"**/logs",
|
||||
"**/tests"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user