builddrone 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
builddrone/__init__.py ADDED
File without changes
builddrone/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Builddrone module entry point."""
2
+
3
+ from builddrone.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,19 @@
1
+ """Base module definition for builddrone modules."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from builddrone.runner import Runner
6
+
7
+
8
+ class BaseModule(ABC): # pylint: disable=too-few-public-methods
9
+ """Base class for all builddrone modules."""
10
+
11
+ @abstractmethod
12
+ def run(self, runner: Runner, args: dict) -> None:
13
+ """Execute the module.
14
+
15
+ Args:
16
+ runner: Runner instance used to execute commands.
17
+ args: Module configuration arguments.
18
+ """
19
+ raise NotImplementedError
builddrone/cli.py ADDED
@@ -0,0 +1,30 @@
1
+ """Command line interface for Builddrone."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from collections.abc import Sequence
7
+
8
+ from builddrone.drone_exception import DroneException
9
+ from builddrone.execution_engine import ExecutionEngine
10
+
11
+
12
+ def build_parser() -> argparse.ArgumentParser:
13
+ """Create the Builddrone CLI parser."""
14
+ parser = argparse.ArgumentParser(prog="python -m builddrone")
15
+ parser.add_argument("stage", help="Pipeline stage to execute")
16
+ return parser
17
+
18
+
19
+ def main(argv: Sequence[str] | None = None) -> int:
20
+ """Run the Builddrone CLI."""
21
+ parser = build_parser()
22
+ args = parser.parse_args(argv)
23
+
24
+ try:
25
+ engine = ExecutionEngine({})
26
+ engine.run(args.stage)
27
+ except DroneException as exc:
28
+ parser.exit(1, f"{parser.prog}: error: {exc}\n")
29
+
30
+ return 0
@@ -0,0 +1,10 @@
1
+ """Custom exceptions for the Builddrone package."""
2
+
3
+
4
+ class DroneException(Exception):
5
+ """Base exception raised by Builddrone-specific runtime errors.
6
+
7
+ Use this exception as the common parent for errors that originate from
8
+ the Builddrone domain, so callers can catch framework-level failures in a
9
+ single place while still allowing more specific exception types later.
10
+ """
@@ -0,0 +1,89 @@
1
+ """Execution engine for running builddrone pipeline stages."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from builddrone.base_module import BaseModule
7
+ from builddrone.drone_exception import DroneException
8
+ from builddrone.module.filesystem.cleanup_module import (
9
+ CleanupModule as FilesystemCleanupModule,
10
+ )
11
+ from builddrone.module.filesystem.copy_module import FilesystemCopyModule
12
+ from builddrone.module.python.build_module import PythonBuildModule
13
+ from builddrone.module.python.install_module import PythonInstallModule
14
+ from builddrone.module.python.pylint_module import PylintModule
15
+ from builddrone.module.python.run_module import PythonRunModule
16
+ from builddrone.module.python.venv_module import PythonVirtualEnvironmentModule
17
+ from builddrone.module.robotframework.rebot_module import RobotframeworkRebotModule
18
+ from builddrone.module.robotframework.test_module import RobotframeworkTestModule
19
+ from builddrone.runner import Runner
20
+
21
+
22
+ class ExecutionEngine: # pylint: disable=too-few-public-methods
23
+ """Execute configured builddrone stages."""
24
+
25
+ def __init__(self, modules: dict[str, BaseModule]):
26
+ """Initialize the execution engine.
27
+
28
+ Args:
29
+ modules: Registered builddrone modules.
30
+ """
31
+ self._modules = modules
32
+ self._config_path = Path("blueprint.json")
33
+ self._register_module("filesystem.cleanup", FilesystemCleanupModule())
34
+ self._register_module("filesystem.copy", FilesystemCopyModule())
35
+ self._register_module("python.build", PythonBuildModule())
36
+ self._register_module("python.install", PythonInstallModule())
37
+ self._register_module("python.run", PythonRunModule())
38
+ self._register_module("python.venv", PythonVirtualEnvironmentModule())
39
+ self._register_module("python.pylint", PylintModule())
40
+ self._register_module("robotframework.test", RobotframeworkTestModule())
41
+ self._register_module("robotframework.rebot", RobotframeworkRebotModule())
42
+ self._runner = Runner()
43
+
44
+ def run(self, stage: str) -> None:
45
+ """Execute a build stage from a configuration file."""
46
+ config_path = self._config_path
47
+ config_file = config_path.resolve()
48
+ self._runner.set_base_path(config_file.parent)
49
+ config = self._load_config(str(config_path))
50
+
51
+ if stage not in config:
52
+ raise DroneException(f"Stage '{stage}' not found in config")
53
+
54
+ steps = config[stage]
55
+
56
+ if not isinstance(steps, list):
57
+ raise DroneException(f"Stage '{stage}' must be a list of steps")
58
+
59
+ for step in steps:
60
+ self._execute_step(step)
61
+
62
+ def _execute_step(self, step: dict) -> None:
63
+ """Execute a single build step."""
64
+ if not isinstance(step, dict):
65
+ raise DroneException("Pipeline step must be an object")
66
+
67
+ module_name = step.get("module")
68
+
69
+ if module_name is None or not isinstance(module_name, str):
70
+ raise DroneException("Unknown module name")
71
+
72
+ args = step.get("args", {})
73
+ module = self._modules.get(module_name)
74
+
75
+ if module is None:
76
+ raise DroneException(f"Unknown module: {module_name}")
77
+
78
+ module.run(self._runner, args)
79
+
80
+ def _register_module(self, name: str, module: BaseModule) -> None:
81
+ """Register a single build module."""
82
+ if name not in self._modules:
83
+ self._modules[name] = module
84
+
85
+ @staticmethod
86
+ def _load_config(path: str) -> dict:
87
+ """Load build configuration from JSON."""
88
+ with open(path, encoding="utf-8") as file:
89
+ return json.load(file)
File without changes
File without changes
@@ -0,0 +1,70 @@
1
+ """Filesystem cleanup module."""
2
+
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ from builddrone.base_module import BaseModule
8
+ from builddrone.drone_exception import DroneException
9
+ from builddrone.runner import Runner
10
+
11
+
12
+ class CleanupModule(BaseModule): # pylint: disable=too-few-public-methods
13
+ """A module responsible for cleanup operations.
14
+
15
+ This module handles cleanup, such as removing temporary files or folders.
16
+
17
+ Blueprint configuration arguments:
18
+ "files": ["List of files to delete"]
19
+ "folders": ["List of folders to delete"]
20
+ """
21
+
22
+ def run(self, runner: Runner, args: dict) -> None:
23
+ runner.logger.info("Cleaning up...")
24
+ base_path = Path(runner.get_base_path())
25
+ self._delete_files(runner, args.get("files", []), base_path)
26
+ self._delete_folders(runner, args.get("folders", []), base_path)
27
+
28
+ @staticmethod
29
+ def _delete_files(runner: Runner, files: list, base_path: Path):
30
+ for file in files:
31
+ resolved_file = Path(file)
32
+ if not os.path.isabs(file):
33
+ resolved_file = base_path / file
34
+
35
+ if not os.path.exists(resolved_file):
36
+ # If file not exists, nothing to delete
37
+ continue
38
+
39
+ try:
40
+ if os.path.isfile(resolved_file):
41
+ os.remove(resolved_file)
42
+ runner.logger.info(f"Deleted file: {resolved_file}")
43
+ else:
44
+ raise DroneException(f"Is not a file: {resolved_file}")
45
+ except OSError as e:
46
+ msg = f"Error deleting file {resolved_file} : {e}"
47
+ runner.logger.error(msg)
48
+ raise DroneException(msg) from e
49
+
50
+ @staticmethod
51
+ def _delete_folders(runner: Runner, folders: list, base_path: Path):
52
+ for folder in folders:
53
+ resolved_folder = Path(folder)
54
+ if not os.path.isabs(folder):
55
+ resolved_folder = base_path / folder
56
+
57
+ if not os.path.exists(resolved_folder):
58
+ # If folder not exists, nothing to delete
59
+ continue
60
+
61
+ try:
62
+ if os.path.isdir(resolved_folder):
63
+ shutil.rmtree(resolved_folder)
64
+ runner.logger.info(f"Deleted folder: {resolved_folder}")
65
+ else:
66
+ raise DroneException(f"Is not a folder: {resolved_folder}")
67
+ except OSError as e:
68
+ msg = f"Error deleting folder {resolved_folder} : {e}"
69
+ runner.logger.error(msg)
70
+ raise DroneException(msg) from e
@@ -0,0 +1,72 @@
1
+ """Filesystem copy module."""
2
+
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ from builddrone.base_module import BaseModule
8
+ from builddrone.drone_exception import DroneException
9
+ from builddrone.runner import Runner
10
+
11
+
12
+ class FilesystemCopyModule(BaseModule): # pylint: disable=too-few-public-methods
13
+ """A module responsible for copying files from one folder to another.
14
+
15
+ Blueprint configuration arguments:
16
+ "source": "Source directory to copy from"
17
+ "destination": "Destination directory to copy to"
18
+ """
19
+
20
+ def run(self, runner: Runner, args: dict) -> None:
21
+ """Copy all files from a source directory into a destination directory."""
22
+ runner.logger.info("Copying files...")
23
+ source = args.get("source")
24
+ destination = args.get("destination")
25
+ base_path = Path(runner.get_base_path())
26
+
27
+ if not isinstance(source, str) or not source:
28
+ raise DroneException("No source provided for copy")
29
+
30
+ if not isinstance(destination, str) or not destination:
31
+ raise DroneException("No destination provided for copy")
32
+
33
+ self._copy_tree(runner, source, destination, base_path)
34
+
35
+ @staticmethod
36
+ def _copy_tree(
37
+ runner: Runner, source: str, destination: str, base_path: Path
38
+ ) -> None:
39
+ """Copy a directory tree preserving relative paths."""
40
+ source_path = Path(source)
41
+ destination_path = Path(destination)
42
+
43
+ if not os.path.isabs(source):
44
+ source_path = base_path / source_path
45
+
46
+ if not os.path.isabs(destination):
47
+ destination_path = base_path / destination_path
48
+
49
+ if not os.path.isdir(source_path):
50
+ raise DroneException(f"Source is not a directory: {source_path}")
51
+
52
+ os.makedirs(destination_path, exist_ok=True)
53
+
54
+ for root, _, files in os.walk(source_path):
55
+ relative_root = os.path.relpath(root, source_path)
56
+ target_root = (
57
+ destination_path
58
+ if relative_root == "."
59
+ else os.path.join(destination_path, relative_root)
60
+ )
61
+ os.makedirs(target_root, exist_ok=True)
62
+
63
+ for file_name in files:
64
+ source_file = os.path.join(root, file_name)
65
+ destination_file = os.path.join(target_root, file_name)
66
+ try:
67
+ shutil.copy2(source_file, destination_file)
68
+ runner.logger.info("Copied file: %s", source_file)
69
+ except OSError as exc:
70
+ msg = f"Error copying file {source_file} : {exc}"
71
+ runner.logger.error(msg)
72
+ raise DroneException(msg) from exc
File without changes
@@ -0,0 +1,26 @@
1
+ """Python build module."""
2
+
3
+ from builddrone.base_module import BaseModule
4
+ from builddrone.drone_exception import DroneException
5
+ from builddrone.runner import Runner
6
+
7
+
8
+ class PythonBuildModule(BaseModule): # pylint: disable=too-few-public-methods
9
+ """A module responsible for building the project.
10
+
11
+ Blueprint configuration arguments:
12
+ None
13
+ """
14
+
15
+ def run(self, runner: Runner, _args: dict) -> None:
16
+ """Build the project with ``python -m build``.
17
+
18
+ Args:
19
+ runner: Runner instance used to execute commands.
20
+ _args: Module configuration arguments. Unused.
21
+ """
22
+ runner.logger.info("Building...")
23
+ exit_code = runner.run(["-m", "build"], cwd=str(runner.get_base_path()))
24
+
25
+ if exit_code != 0:
26
+ raise DroneException(f"Build failed with exit code {exit_code}")
@@ -0,0 +1,40 @@
1
+ """Python install module."""
2
+
3
+ from builddrone.base_module import BaseModule
4
+ from builddrone.drone_exception import DroneException
5
+ from builddrone.runner import Runner
6
+
7
+
8
+ class PythonInstallModule(BaseModule): # pylint: disable=too-few-public-methods
9
+ """A module responsible for installing Python packages.
10
+
11
+ Blueprint configuration arguments:
12
+ "source": "Package source to install"
13
+ "requirements": "Requirements file to install"
14
+ """
15
+
16
+ def run(self, runner: Runner, args: dict) -> None:
17
+ """Install a package source with ``python -m pip install``.
18
+
19
+ Args:
20
+ runner: Runner instance used to execute commands.
21
+ args: Module configuration arguments.
22
+ """
23
+ runner.logger.info("Installing...")
24
+ source = args.get("source")
25
+ requirements = args.get("requirements")
26
+
27
+ if isinstance(requirements, str) and requirements:
28
+ install_args = ["-r", requirements]
29
+ elif isinstance(source, str) and source:
30
+ install_args = [source]
31
+ else:
32
+ raise DroneException("No source or requirements provided for install")
33
+
34
+ exit_code = runner.run(
35
+ ["-m", "pip", "install", *install_args],
36
+ cwd=str(runner.get_base_path()),
37
+ )
38
+
39
+ if exit_code != 0:
40
+ raise DroneException(f"Install failed with exit code {exit_code}")
@@ -0,0 +1,49 @@
1
+ """Python linting module."""
2
+
3
+ from builddrone.base_module import BaseModule
4
+ from builddrone.drone_exception import DroneException
5
+ from builddrone.runner import Runner
6
+
7
+
8
+ class PylintModule(BaseModule): # pylint: disable=too-few-public-methods
9
+ """A module responsible for running pylint.
10
+
11
+ Blueprint configuration arguments:
12
+ "paths": ["List of paths to lint"]
13
+ "files": ["Optional list of individual Python files to lint"]
14
+ "ignore": ["Optional list of file or directory base names to skip"]
15
+ """
16
+
17
+ def run(self, runner: Runner, args: dict) -> None:
18
+ runner.logger.info("Pylint...")
19
+ paths = args.get("paths", [])
20
+ files = args.get("files", [])
21
+ ignore = args.get("ignore", [])
22
+
23
+ if not isinstance(paths, list) or not all(
24
+ isinstance(item, str) and item for item in paths
25
+ ):
26
+ raise DroneException("Paths must be a list of non-empty strings")
27
+
28
+ if not isinstance(files, list) or not all(
29
+ isinstance(item, str) and item for item in files
30
+ ):
31
+ raise DroneException("Files must be a list of non-empty strings")
32
+
33
+ if not paths and not files:
34
+ raise DroneException("No paths or files provided for pylint")
35
+
36
+ if not isinstance(ignore, list) or not all(
37
+ isinstance(item, str) and item for item in ignore
38
+ ):
39
+ raise DroneException("Ignore must be a list of non-empty strings")
40
+
41
+ command = ["-m", "pylint"]
42
+ if ignore:
43
+ command.extend(["--ignore", ",".join(ignore)])
44
+ command.extend([*paths, *files])
45
+
46
+ exit_code = runner.run(command, cwd=str(runner.get_base_path()))
47
+
48
+ if exit_code != 0:
49
+ raise DroneException(f"Pylint failed with exit code {exit_code}")
@@ -0,0 +1,31 @@
1
+ """Python run module."""
2
+
3
+ from builddrone.base_module import BaseModule
4
+ from builddrone.drone_exception import DroneException
5
+ from builddrone.runner import Runner
6
+
7
+
8
+ class PythonRunModule(BaseModule): # pylint: disable=too-few-public-methods
9
+ """A module responsible for running Python source files.
10
+
11
+ Blueprint configuration arguments:
12
+ "source": "Python file to execute"
13
+ """
14
+
15
+ def run(self, runner: Runner, args: dict) -> None:
16
+ """Run a Python source file with the configured interpreter.
17
+
18
+ Args:
19
+ runner: Runner instance used to execute commands.
20
+ args: Module configuration arguments.
21
+ """
22
+ runner.logger.info("Running...")
23
+ source = args.get("source")
24
+
25
+ if not isinstance(source, str) or not source:
26
+ raise DroneException("No source provided for run")
27
+
28
+ exit_code = runner.run([source], cwd=str(runner.get_base_path()))
29
+
30
+ if exit_code != 0:
31
+ raise DroneException(f"Run failed with exit code {exit_code}")
@@ -0,0 +1,68 @@
1
+ """Python virtual environment module."""
2
+
3
+ import subprocess
4
+ import venv
5
+ from pathlib import Path
6
+
7
+ from builddrone.base_module import BaseModule
8
+ from builddrone.drone_exception import DroneException
9
+ from builddrone.runner import Runner
10
+
11
+
12
+ class PythonVirtualEnvironmentModule(
13
+ BaseModule
14
+ ): # pylint: disable=too-few-public-methods
15
+ """A module responsible for configuring a Python virtual environment.
16
+
17
+ Blueprint configuration arguments:
18
+ "source": "Virtual environment root path to use; empty string resets the runner"
19
+ """
20
+
21
+ def run(self, runner: Runner, args: dict) -> None:
22
+ """Configure the runner to use a virtual environment interpreter."""
23
+ source = args.get("source")
24
+
25
+ if source == "":
26
+ runner.logger.info("Resetting runner to the current Python interpreter...")
27
+ runner.reset_runner()
28
+ return
29
+
30
+ if not isinstance(source, str) or not source:
31
+ raise DroneException("No source provided for virtual environment")
32
+
33
+ venv_path = Path(source)
34
+ if not venv_path.is_absolute():
35
+ venv_path = Path(runner.get_base_path()) / venv_path
36
+
37
+ python_executable = self._resolve_python_executable(venv_path)
38
+
39
+ if python_executable is None:
40
+ runner.logger.info("Creating virtual environment: %s", venv_path)
41
+ try:
42
+ venv.create(venv_path, with_pip=True)
43
+ except (OSError, RuntimeError, subprocess.CalledProcessError) as exception:
44
+ raise DroneException(
45
+ f"Could not create virtual environment: {venv_path}"
46
+ ) from exception
47
+
48
+ python_executable = self._resolve_python_executable(venv_path)
49
+
50
+ if python_executable is None:
51
+ raise DroneException(f"Invalid virtual environment path: {venv_path}")
52
+
53
+ runner.logger.info("Using virtual environment: %s", venv_path)
54
+ runner.set_runner(str(python_executable))
55
+
56
+ @staticmethod
57
+ def _resolve_python_executable(venv_path: Path) -> Path | None:
58
+ """Resolve the interpreter path inside the virtual environment."""
59
+ candidate_paths = (
60
+ venv_path / "Scripts" / "python.exe",
61
+ venv_path / "bin" / "python",
62
+ )
63
+
64
+ for candidate in candidate_paths:
65
+ if candidate.is_file():
66
+ return candidate
67
+
68
+ return None
File without changes
@@ -0,0 +1,20 @@
1
+ """Robot Framework rebot module."""
2
+
3
+ from builddrone.module.robotframework.robotframework_base_module import (
4
+ RobotframeworkBaseModule,
5
+ )
6
+
7
+
8
+ class RobotframeworkRebotModule( # pylint: disable=too-few-public-methods
9
+ RobotframeworkBaseModule
10
+ ):
11
+ """A module responsible for running Robot Framework's rebot command.
12
+
13
+ Blueprint configuration arguments:
14
+ "arguments": "Dictionary of CLI arguments to pass to rebot"
15
+ "cwd": "Optional working directory to run rebot from"
16
+ """
17
+
18
+ command_prefix = ["-m", "robot.rebot"]
19
+ log_message = "Rebot..."
20
+ failure_label = "Rebot"
@@ -0,0 +1,70 @@
1
+ """Shared helpers for Robot Framework modules."""
2
+
3
+ from abc import ABC
4
+ from pathlib import Path
5
+
6
+ from builddrone.base_module import BaseModule
7
+ from builddrone.drone_exception import DroneException
8
+ from builddrone.runner import Runner
9
+
10
+
11
+ class RobotframeworkBaseModule(
12
+ BaseModule, ABC
13
+ ): # pylint: disable=too-few-public-methods
14
+ """Common behavior for Robot Framework command modules."""
15
+
16
+ command_prefix: list[str] = []
17
+ log_message: str = ""
18
+ failure_label: str = ""
19
+
20
+ def run(self, runner: Runner, args: dict) -> None:
21
+ """Run a Robot Framework command with expanded CLI arguments."""
22
+ runner.logger.info(self.log_message)
23
+ arguments = args.get("arguments", {})
24
+ cwd = args.get("cwd")
25
+
26
+ if not isinstance(arguments, dict):
27
+ raise DroneException("Arguments must be a dictionary")
28
+
29
+ if cwd is not None and not isinstance(cwd, (str, Path)):
30
+ raise DroneException("Cwd must be a path or string")
31
+
32
+ working_directory = Path(runner.get_base_path())
33
+ if cwd is not None:
34
+ configured_cwd = Path(cwd)
35
+ working_directory = (
36
+ configured_cwd
37
+ if configured_cwd.is_absolute()
38
+ else working_directory / configured_cwd
39
+ )
40
+
41
+ command = self._build_command(arguments)
42
+ exit_code = runner.run(command, cwd=str(working_directory))
43
+
44
+ if exit_code != 0:
45
+ raise DroneException(
46
+ f"{self.failure_label} failed with exit code {exit_code}"
47
+ )
48
+
49
+ def _build_command(self, arguments: dict) -> list[str]:
50
+ """Convert a dictionary of CLI options into a Robot Framework command."""
51
+ command: list[str] = list(self.command_prefix)
52
+
53
+ for key, value in arguments.items():
54
+ if isinstance(value, bool):
55
+ if value:
56
+ command.append(str(key))
57
+ continue
58
+
59
+ if isinstance(value, (list, tuple)):
60
+ command.append(str(key))
61
+ command.extend(str(item) for item in value)
62
+ continue
63
+
64
+ if value is None:
65
+ command.append(str(key))
66
+ continue
67
+
68
+ command.extend([str(key), str(value)])
69
+
70
+ return command
@@ -0,0 +1,20 @@
1
+ """Robot Framework test module."""
2
+
3
+ from builddrone.module.robotframework.robotframework_base_module import (
4
+ RobotframeworkBaseModule,
5
+ )
6
+
7
+
8
+ class RobotframeworkTestModule( # pylint: disable=too-few-public-methods
9
+ RobotframeworkBaseModule
10
+ ):
11
+ """A module responsible for running Robot Framework test suites.
12
+
13
+ Blueprint configuration arguments:
14
+ "arguments": "Dictionary of CLI arguments to pass to robot"
15
+ "cwd": "Optional working directory to run robot from"
16
+ """
17
+
18
+ command_prefix = ["-m", "robot"]
19
+ log_message = "Robot..."
20
+ failure_label = "Robot"
builddrone/runner.py ADDED
@@ -0,0 +1,52 @@
1
+ """Runner module for executing build commands."""
2
+
3
+ import logging
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from builddrone.drone_exception import DroneException
10
+
11
+
12
+ class Runner:
13
+ """Execute build commands using a configured Python interpreter."""
14
+
15
+ def __init__(self):
16
+ """Initialize the runner."""
17
+ logging.basicConfig(level=logging.INFO)
18
+ self.logger = logging.getLogger(__name__)
19
+
20
+ self._python_path = sys.executable
21
+ self._base_path: Path | None = None
22
+ if not self._python_path:
23
+ raise DroneException("Python executable not found")
24
+
25
+ def set_runner(self, python_path):
26
+ """Set the Python executable used for command execution."""
27
+ if os.path.exists(python_path) and os.path.isfile(python_path):
28
+ self._python_path = python_path
29
+
30
+ def reset_runner(self):
31
+ """Reset the runner to use the current Python interpreter."""
32
+ self._python_path = sys.executable
33
+ if not self._python_path:
34
+ raise DroneException("Python executable not found")
35
+
36
+ def set_base_path(self, base_path: str | Path | None) -> None:
37
+ """Set the base directory used for relative paths."""
38
+ self._base_path = None if base_path is None else Path(base_path)
39
+
40
+ def get_base_path(self) -> Path:
41
+ """Return the base directory used for relative paths."""
42
+ return self._base_path or Path.cwd()
43
+
44
+ def run(self, cmd, cwd=None) -> int:
45
+ """Execute a Python command and return the exit code."""
46
+ full_cmd = [self._python_path] + cmd
47
+ result = subprocess.run(
48
+ full_cmd,
49
+ cwd=cwd,
50
+ check=False,
51
+ )
52
+ return result.returncode
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: builddrone
3
+ Version: 0.1.0
4
+ Summary: Modular build framework
5
+ Author: Andreas Sekulski
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Provides-Extra: dev
9
+ Requires-Dist: build; extra == "dev"
10
+ Requires-Dist: twine; extra == "dev"
11
+ Requires-Dist: pytest; extra == "dev"
12
+ Requires-Dist: black; extra == "dev"
13
+ Requires-Dist: isort; extra == "dev"
14
+ Requires-Dist: pylint; extra == "dev"
15
+
16
+ # Builddrone
17
+
18
+ ![Builddrone](logo.png)
19
+
20
+ Builddrone is a JSON-driven build orchestration framework and command-line
21
+ interface for Python projects. A pipeline is made up of named stages, and each
22
+ stage executes an ordered list of registered modules.
23
+
24
+ ## Installation
25
+
26
+ Install the project and its development tools from the repository root:
27
+
28
+ ```bash
29
+ python -m pip install -e ".[dev]"
30
+ ```
31
+
32
+ Builddrone requires Python 3.10 or newer.
33
+
34
+ ## Blueprint format
35
+
36
+ Builddrone loads `blueprint.json` from the current working directory. The
37
+ top-level keys are stage names, and each stage contains module steps:
38
+
39
+ ```json
40
+ {
41
+ "build": [
42
+ {
43
+ "module": "python.venv",
44
+ "args": {"source": ".venv"}
45
+ },
46
+ {
47
+ "module": "python.install",
48
+ "args": {"requirements": "requirements.txt"}
49
+ },
50
+ {
51
+ "module": "python.run",
52
+ "args": {"source": "main.py"}
53
+ }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ Relative paths are resolved from the directory containing `blueprint.json`.
59
+ Each step runs only after the preceding step succeeds.
60
+
61
+ ## Command-line usage
62
+
63
+ Run a stage from the directory containing its blueprint:
64
+
65
+ ```bash
66
+ cd example/python
67
+ python -m builddrone build
68
+ python -m builddrone cleanup
69
+ ```
70
+
71
+ The Python example creates `.venv` automatically, installs its requirements,
72
+ lints and runs `main.py`, and builds a wheel and source distribution with
73
+ `python -m build`. The separate `cleanup` stage removes the temporary
74
+ environment and generated build artifacts.
75
+
76
+ The Robot Framework example runs similarly:
77
+
78
+ ```bash
79
+ cd example/robotframework
80
+ python -m builddrone test
81
+ python -m builddrone cleanup
82
+ ```
83
+
84
+ Its `robotframework.test` step writes `results/output.xml`, and
85
+ `robotframework.rebot` converts that result into reports under
86
+ `results/rebot`.
87
+
88
+ ## Built-in modules
89
+
90
+ ### Filesystem
91
+
92
+ | Module | Arguments | Purpose |
93
+ | --- | --- | --- |
94
+ | `filesystem.copy` | `source`, `destination` | Copy a directory tree. |
95
+ | `filesystem.cleanup` | `files`, `folders` | Remove listed files and folders. |
96
+
97
+ ### Python
98
+
99
+ | Module | Arguments | Purpose |
100
+ | --- | --- | --- |
101
+ | `python.venv` | `source` | Select an environment, creating it with pip if needed. An empty source resets the runner. |
102
+ | `python.install` | `requirements` or `source` | Install requirements or a package with pip. |
103
+ | `python.run` | `source` | Run a Python source file. |
104
+ | `python.build` | none | Run `python -m build`. |
105
+ | `python.pylint` | `paths`, `files`, `ignore` | Run Pylint against paths or individual files. |
106
+
107
+ ### Robot Framework
108
+
109
+ Both Robot Framework modules accept an `arguments` dictionary and an optional
110
+ `cwd`:
111
+
112
+ | Module | Purpose |
113
+ | --- | --- |
114
+ | `robotframework.test` | Run `python -m robot`. |
115
+ | `robotframework.rebot` | Run `python -m robot.rebot` to post-process results. |
116
+
117
+ Dictionary values are converted to command-line arguments. A `null` value is a
118
+ flag or positional argument, a string receives a following value, and a list
119
+ receives multiple values:
120
+
121
+ ```json
122
+ {
123
+ "module": "robotframework.test",
124
+ "args": {
125
+ "arguments": {
126
+ "--outputdir": "results",
127
+ "tests": null
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ ## Using Builddrone as a framework
134
+
135
+ Built-in modules are registered by `ExecutionEngine`. Custom modules can be
136
+ added by passing them to the constructor:
137
+
138
+ ```python
139
+ from builddrone.base_module import BaseModule
140
+ from builddrone.execution_engine import ExecutionEngine
141
+
142
+
143
+ class GreetingModule(BaseModule):
144
+ def run(self, runner, args):
145
+ runner.logger.info("Hello from %s", args.get("name", "Builddrone"))
146
+
147
+
148
+ engine = ExecutionEngine({"custom.greeting": GreetingModule()})
149
+ engine.run("build")
150
+ ```
151
+
152
+ The custom module can then be used in `blueprint.json`:
153
+
154
+ ```json
155
+ {
156
+ "build": [
157
+ {
158
+ "module": "custom.greeting",
159
+ "args": {"name": "Builddrone"}
160
+ }
161
+ ]
162
+ }
163
+ ```
164
+
165
+ Every module receives a `Runner` and a dictionary of arguments. The runner
166
+ executes commands with the currently selected Python interpreter and exposes
167
+ the blueprint's base directory for relative paths.
168
+
169
+ ## Execution flow
170
+
171
+ ```text
172
+ python -m builddrone <stage>
173
+ |
174
+ v
175
+ load blueprint.json -> select stage -> execute steps -> report failures
176
+ ```
177
+
178
+ Any non-zero command exit code raises `DroneException` and stops the stage.
179
+
180
+ ## License
181
+
182
+ MIT License
@@ -0,0 +1,25 @@
1
+ builddrone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ builddrone/__main__.py,sha256=ywngXh63RbvJp9nA2PJmI8n3Cqh044yywZe4_NG2P3s,127
3
+ builddrone/base_module.py,sha256=qFOM19wOY-RidGMiEzsrS5-G2aHHQYZRzr2bG3gctEc,524
4
+ builddrone/cli.py,sha256=mmC7f2dvkJ_2JzSmBNgMxgaQIkkpeQGKf4ETduQypJM,824
5
+ builddrone/drone_exception.py,sha256=Hb8FW7nnX3l1U5BbfNToulMuzhEv2sOJlKWX269U_8Q,392
6
+ builddrone/execution_engine.py,sha256=xrBAvyNynIJm3j5gy2LCMLW6BIf2v2Btwz6omXj6kYw,3597
7
+ builddrone/runner.py,sha256=rM3cvaNJrJEDfZ3S_LF2LflZV-mti0uhRqV4PTyR_yU,1730
8
+ builddrone/module/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ builddrone/module/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ builddrone/module/filesystem/cleanup_module.py,sha256=7BfH1Z9W5bgSahGYatrRp6KUTLLiidePgp-c-oJVneA,2602
11
+ builddrone/module/filesystem/copy_module.py,sha256=yH8lwtRBnWbxFeQkrlbcZaufc3ywyQOX73jz-9h46UY,2688
12
+ builddrone/module/python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ builddrone/module/python/build_module.py,sha256=lSPzzME9HsaZgyYVsXVjLtvdw1xQexQuyCafdPGFHjg,849
14
+ builddrone/module/python/install_module.py,sha256=r5-FMbfpIu8uwKzilEZnAK-VT5D3k0pRzGwt-booeIw,1391
15
+ builddrone/module/python/pylint_module.py,sha256=pKs8WlP7IzHxjbimfuNxXxyGJ7gaifhfX5nrAVZPm1U,1795
16
+ builddrone/module/python/run_module.py,sha256=W55SGS4NtkD2U5oa5u--Mwa0iAuktJCE0xZCKCMJVoY,1034
17
+ builddrone/module/python/venv_module.py,sha256=P6QIMsCQ3OssBXCXulMbIwm9JPAKqjrYoRYjW-7aOTI,2392
18
+ builddrone/module/robotframework/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ builddrone/module/robotframework/rebot_module.py,sha256=EszcfKSR_TT7rmCBkXel-wZQEadlCUCC2ul2hDfYgnk,607
20
+ builddrone/module/robotframework/robotframework_base_module.py,sha256=v1KWtBaAgnLX4SAqJBmWpAqxDF4F79OMOgdkret18rw,2287
21
+ builddrone/module/robotframework/test_module.py,sha256=4md-kQMXI3D4I8zvM2AvesU0WqY0AMoJ_W-lHkY5P7Y,595
22
+ builddrone-0.1.0.dist-info/METADATA,sha256=1yrHfgdkcynN4m3lmmIUZNhxAJfYPDRdwev4mDjQpVY,4624
23
+ builddrone-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
24
+ builddrone-0.1.0.dist-info/top_level.txt,sha256=gUWOrFMfu2uJaRS6sDDChaoIsMOVkzqKU_SPDMkKRQ8,11
25
+ builddrone-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ builddrone