module-runner 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: module-runner
3
+ Version: 0.1.0
4
+ Summary: Lightweight helper that prepares Python environments and executes standalone modules.
5
+ Author: Leo Jaimesson
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # Module Runner
12
+
13
+ Module Runner sits in the gap between ad-hoc scripts and heavyweight orchestration frameworks. It gives each module a predictable, isolated environment without asking you to bolt on extra infrastructure, schedulers, or workflow engines. The project stays intentionally lightweight and focuses on two responsibilities:
14
+
15
+ 1. Detect the proper execution strategy (system Python, `venv`, or `uv`).
16
+ 2. Launch the module with an optional JSON payload, returning the `subprocess.CompletedProcess` so you can decide how to consume `stdout`, `stderr`, or exit codes.
17
+
18
+ Any higher-level orchestration (pipelines, RPC, shared storage, etc.) is an application concern, keeping this library small and predictable.
19
+
20
+ ## Features
21
+
22
+ - ⚙️ **Automatic environment discovery**: detects `pyproject.toml` or `requirements.txt` (→ `uv` when available, `venv` as fallback), or falls back to the current interpreter.
23
+ - 📦 **Per-module dependencies**: each module lives in its own folder with its own toolchain, avoiding cross-contamination.
24
+ - 🧱 **Opinionated boundary**: no implicit payload chaining—what a module prints, writes, or exposes is entirely up to you.
25
+ - 🪪 **Clear error surface**: `RunnerExecutionError` captures module failures while missing tooling raises descriptive `RuntimeError`s, keeping debugging straightforward.
26
+ - 🔍 **Built-in logging**: uses Python's standard `logging` module under the `module_runner` logger — enable `DEBUG` to see the resolved strategy, every setup command, and the final execution command.
27
+
28
+ ## Logging
29
+
30
+ Module Runner emits `INFO`-level log records under the `module_runner` logger. To see them:
31
+
32
+ ```python
33
+ import logging
34
+ logging.getLogger("module_runner").setLevel(logging.INFO)
35
+ logging.basicConfig()
36
+ ```
37
+
38
+ Example output:
39
+ ```
40
+ [normalize] strategy=pip (requirements.txt detected, uv unavailable — fallback to venv+pip)
41
+ [normalize] creating venv: /usr/bin/python3 -m venv .venv
42
+ [normalize] installing deps: .venv/bin/python -m pip install -r requirements.txt
43
+ [normalize] executing: .venv/bin/python main.py {"text": " Hello World "}
44
+ ```
45
+
46
+ ## Why This Exists
47
+
48
+ Module Runner is the missing middle layer between one-off helper scripts and enterprise orchestrators. It gives you just enough structure to keep automation tidy while remaining lightweight, infrastructure-free, and dependency-light.
49
+
50
+ ## When to Use
51
+
52
+ Use Module Runner when you:
53
+
54
+ - Need simple local automation or scripting glue with a lightweight footprint
55
+ - Have dependency conflicts between scripts and want per-module isolation
56
+ - Want predictable, sequential execution you can reason about
57
+ - Prefer to stay container-free for lightweight tasks
58
+ - Do not need a workflow engine or task scheduler
59
+
60
+ ## When Not to Use
61
+
62
+ Reach for other tooling if you require:
63
+
64
+ - Complex DAG dependencies or branching workflows
65
+ - Scheduling, cron-style orchestration, or SLAs
66
+ - Distributed workers or autoscaling fleets
67
+ - Monitoring dashboards, retries, or alerting UI
68
+ - Enterprise workflow/orchestration guarantees
69
+
70
+ ## Philosophy
71
+
72
+ - Small, explicit, predictable, and lightweight
73
+ - Infrastructure-free: relies on the Python already on your machine
74
+ - Focused on one problem—launching modules in clean environments
75
+
76
+ It is not meant to be a workflow engine; it simply keeps isolated scripts manageable.
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ # using pip
82
+ pip install module_runner
83
+
84
+ # using uv
85
+ uv add module_runner
86
+
87
+ ```
88
+
89
+ You only need Python 3.9+ and whichever tooling your modules request (e.g., `uv`, `venv`, system packages).
90
+
91
+ ## Quick Start
92
+
93
+ ```python
94
+ from module_runner import Runner
95
+
96
+ runner = Runner(module_path="modules/normalize_text")
97
+
98
+ process = runner.run(
99
+ payload={"text": " Hello World "},
100
+ )
101
+
102
+ print(process.stdout) # or json.loads(process.stdout)
103
+ ```
104
+
105
+ Your module layout needs a `main.py` entry point. Any payload you pass is serialized as JSON and delivered as the last CLI argument. How you emit results (stdout, files, sockets) is entirely your call.
106
+
107
+ ## Environment Selection
108
+
109
+ | Signal in module folder | Mode used | Requirement |
110
+ | ----------------------- | ------------- | ----------- |
111
+ | `pyproject.toml` | `uv run ...` | `uv` available in `PATH` |
112
+ | `pyproject.toml` (no `uv`) | `python -m venv` + `pip install .` | `venv` module available |
113
+ | `requirements.txt` | `uv venv` + `uv pip install -r requirements.txt` | `uv` available in `PATH` |
114
+ | `requirements.txt` (no `uv`) | `python -m venv` + `pip install -r requirements.txt` | `venv` module available |
115
+ | none of the above | current interpreter (`sys.executable`) | none |
116
+
117
+ - Auto-detection always checks whether the required tooling is installed. When `uv` is available it is preferred for both `pyproject.toml` and `requirements.txt` modules. If `uv` is not available, both `pyproject.toml` and `requirements.txt` modules fall back to `venv`/`pip`.
118
+ - If a module signals that it needs `uv` or `venv` but the corresponding tooling is missing, Module Runner raises a descriptive `RuntimeError` explaining what needs to be installed.
119
+ - Runtime failures propagate as `RunnerExecutionError`, exposing the module name, exit code, captured `stderr`, captured `stdout`, and the exact `cmd` list that was executed — making it straightforward to reproduce or log failures.
120
+
121
+ ## Sandbox Playground
122
+
123
+ The [sandbox](sandbox/README.md) directory ships with two toy modules (`normalize` and `stats`) that showcase:
124
+
125
+ - How `requirements.txt` triggers a module-specific virtual environment.
126
+ - How `pyproject.toml` causes execution via `uv run`.
127
+ - How you can manually chain modules by parsing the `stdout` from the first run and feeding it into the next.
128
+
129
+ Run everything with:
130
+
131
+ ```bash
132
+ python sandbox/run_examples.py
133
+ ```
134
+
135
+ Use this folder as a template to create your own modules or to test different deployment scenarios.
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ git clone https://github.com/<seu-usuario>/module_runner.git
141
+ cd module_runner
142
+ python -m venv .venv && source .venv/bin/activate
143
+ pip install -e .
144
+ ```
145
+
146
+ Feel free to open issues or pull requests with improvements. The scope intentionally stays small: reliable environment setup and process execution.
147
+
148
+ ## License
149
+
150
+ Module Runner is released under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,140 @@
1
+ # Module Runner
2
+
3
+ Module Runner sits in the gap between ad-hoc scripts and heavyweight orchestration frameworks. It gives each module a predictable, isolated environment without asking you to bolt on extra infrastructure, schedulers, or workflow engines. The project stays intentionally lightweight and focuses on two responsibilities:
4
+
5
+ 1. Detect the proper execution strategy (system Python, `venv`, or `uv`).
6
+ 2. Launch the module with an optional JSON payload, returning the `subprocess.CompletedProcess` so you can decide how to consume `stdout`, `stderr`, or exit codes.
7
+
8
+ Any higher-level orchestration (pipelines, RPC, shared storage, etc.) is an application concern, keeping this library small and predictable.
9
+
10
+ ## Features
11
+
12
+ - ⚙️ **Automatic environment discovery**: detects `pyproject.toml` or `requirements.txt` (→ `uv` when available, `venv` as fallback), or falls back to the current interpreter.
13
+ - 📦 **Per-module dependencies**: each module lives in its own folder with its own toolchain, avoiding cross-contamination.
14
+ - 🧱 **Opinionated boundary**: no implicit payload chaining—what a module prints, writes, or exposes is entirely up to you.
15
+ - 🪪 **Clear error surface**: `RunnerExecutionError` captures module failures while missing tooling raises descriptive `RuntimeError`s, keeping debugging straightforward.
16
+ - 🔍 **Built-in logging**: uses Python's standard `logging` module under the `module_runner` logger — enable `DEBUG` to see the resolved strategy, every setup command, and the final execution command.
17
+
18
+ ## Logging
19
+
20
+ Module Runner emits `INFO`-level log records under the `module_runner` logger. To see them:
21
+
22
+ ```python
23
+ import logging
24
+ logging.getLogger("module_runner").setLevel(logging.INFO)
25
+ logging.basicConfig()
26
+ ```
27
+
28
+ Example output:
29
+ ```
30
+ [normalize] strategy=pip (requirements.txt detected, uv unavailable — fallback to venv+pip)
31
+ [normalize] creating venv: /usr/bin/python3 -m venv .venv
32
+ [normalize] installing deps: .venv/bin/python -m pip install -r requirements.txt
33
+ [normalize] executing: .venv/bin/python main.py {"text": " Hello World "}
34
+ ```
35
+
36
+ ## Why This Exists
37
+
38
+ Module Runner is the missing middle layer between one-off helper scripts and enterprise orchestrators. It gives you just enough structure to keep automation tidy while remaining lightweight, infrastructure-free, and dependency-light.
39
+
40
+ ## When to Use
41
+
42
+ Use Module Runner when you:
43
+
44
+ - Need simple local automation or scripting glue with a lightweight footprint
45
+ - Have dependency conflicts between scripts and want per-module isolation
46
+ - Want predictable, sequential execution you can reason about
47
+ - Prefer to stay container-free for lightweight tasks
48
+ - Do not need a workflow engine or task scheduler
49
+
50
+ ## When Not to Use
51
+
52
+ Reach for other tooling if you require:
53
+
54
+ - Complex DAG dependencies or branching workflows
55
+ - Scheduling, cron-style orchestration, or SLAs
56
+ - Distributed workers or autoscaling fleets
57
+ - Monitoring dashboards, retries, or alerting UI
58
+ - Enterprise workflow/orchestration guarantees
59
+
60
+ ## Philosophy
61
+
62
+ - Small, explicit, predictable, and lightweight
63
+ - Infrastructure-free: relies on the Python already on your machine
64
+ - Focused on one problem—launching modules in clean environments
65
+
66
+ It is not meant to be a workflow engine; it simply keeps isolated scripts manageable.
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ # using pip
72
+ pip install module_runner
73
+
74
+ # using uv
75
+ uv add module_runner
76
+
77
+ ```
78
+
79
+ You only need Python 3.9+ and whichever tooling your modules request (e.g., `uv`, `venv`, system packages).
80
+
81
+ ## Quick Start
82
+
83
+ ```python
84
+ from module_runner import Runner
85
+
86
+ runner = Runner(module_path="modules/normalize_text")
87
+
88
+ process = runner.run(
89
+ payload={"text": " Hello World "},
90
+ )
91
+
92
+ print(process.stdout) # or json.loads(process.stdout)
93
+ ```
94
+
95
+ Your module layout needs a `main.py` entry point. Any payload you pass is serialized as JSON and delivered as the last CLI argument. How you emit results (stdout, files, sockets) is entirely your call.
96
+
97
+ ## Environment Selection
98
+
99
+ | Signal in module folder | Mode used | Requirement |
100
+ | ----------------------- | ------------- | ----------- |
101
+ | `pyproject.toml` | `uv run ...` | `uv` available in `PATH` |
102
+ | `pyproject.toml` (no `uv`) | `python -m venv` + `pip install .` | `venv` module available |
103
+ | `requirements.txt` | `uv venv` + `uv pip install -r requirements.txt` | `uv` available in `PATH` |
104
+ | `requirements.txt` (no `uv`) | `python -m venv` + `pip install -r requirements.txt` | `venv` module available |
105
+ | none of the above | current interpreter (`sys.executable`) | none |
106
+
107
+ - Auto-detection always checks whether the required tooling is installed. When `uv` is available it is preferred for both `pyproject.toml` and `requirements.txt` modules. If `uv` is not available, both `pyproject.toml` and `requirements.txt` modules fall back to `venv`/`pip`.
108
+ - If a module signals that it needs `uv` or `venv` but the corresponding tooling is missing, Module Runner raises a descriptive `RuntimeError` explaining what needs to be installed.
109
+ - Runtime failures propagate as `RunnerExecutionError`, exposing the module name, exit code, captured `stderr`, captured `stdout`, and the exact `cmd` list that was executed — making it straightforward to reproduce or log failures.
110
+
111
+ ## Sandbox Playground
112
+
113
+ The [sandbox](sandbox/README.md) directory ships with two toy modules (`normalize` and `stats`) that showcase:
114
+
115
+ - How `requirements.txt` triggers a module-specific virtual environment.
116
+ - How `pyproject.toml` causes execution via `uv run`.
117
+ - How you can manually chain modules by parsing the `stdout` from the first run and feeding it into the next.
118
+
119
+ Run everything with:
120
+
121
+ ```bash
122
+ python sandbox/run_examples.py
123
+ ```
124
+
125
+ Use this folder as a template to create your own modules or to test different deployment scenarios.
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ git clone https://github.com/<seu-usuario>/module_runner.git
131
+ cd module_runner
132
+ python -m venv .venv && source .venv/bin/activate
133
+ pip install -e .
134
+ ```
135
+
136
+ Feel free to open issues or pull requests with improvements. The scope intentionally stays small: reliable environment setup and process execution.
137
+
138
+ ## License
139
+
140
+ Module Runner is released under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,7 @@
1
+ from .runner import Runner
2
+ from .exceptions import RunnerExecutionError
3
+ from .environment import EnvironmentMode
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["Runner", "RunnerExecutionError", "EnvironmentMode", "__version__"]
@@ -0,0 +1,4 @@
1
+ from .mode import EnvironmentMode
2
+ from .manager import EnvironmentManager
3
+
4
+ __all__ = ["EnvironmentMode", "EnvironmentManager"]
@@ -0,0 +1,4 @@
1
+ PYPROJECT_TOML = "pyproject.toml"
2
+ REQUIREMENTS_TXT = "requirements.txt"
3
+ MAIN_PY = "main.py"
4
+ VENV_DIR = ".venv"
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ from .mode import EnvironmentMode
5
+ from .resolver import resolve_mode
6
+ from .pip_env import ensure_pip_environment
7
+ from .uv_env import ensure_uv_environment
8
+
9
+
10
+ class EnvironmentManager:
11
+
12
+ def __init__(self, module_path: Path, mode: EnvironmentMode | str = EnvironmentMode.AUTO):
13
+ self.module_path = module_path
14
+ self.module_name = module_path.name
15
+ self.mode = self._coerce_mode(mode)
16
+
17
+ def _coerce_mode(self, mode: EnvironmentMode | str) -> EnvironmentMode:
18
+ if isinstance(mode, EnvironmentMode):
19
+ return mode
20
+
21
+ try:
22
+ return EnvironmentMode(mode)
23
+ except ValueError as exc:
24
+ raise RuntimeError(
25
+ f"Unsupported environment mode '{mode}' for module '{self.module_name}'"
26
+ ) from exc
27
+
28
+ def resolve(self) -> EnvironmentMode:
29
+ return resolve_mode(self.module_path, self.mode, self.module_name)
30
+
31
+ def ensure(self) -> Path:
32
+ env_type = self.resolve()
33
+
34
+ if env_type is EnvironmentMode.UV:
35
+ return ensure_uv_environment(self.module_path, self.module_name)
36
+
37
+ if env_type is EnvironmentMode.PIP:
38
+ return ensure_pip_environment(self.module_path, self.module_name)
39
+
40
+ return Path(sys.executable)
@@ -0,0 +1,8 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EnvironmentMode(str, Enum):
5
+ AUTO = "auto"
6
+ UV = "uv"
7
+ PIP = "pip"
8
+ SYSTEM = "system"
@@ -0,0 +1,60 @@
1
+ import importlib.util
2
+ import logging
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .constants import PYPROJECT_TOML, REQUIREMENTS_TXT, VENV_DIR
8
+ from .utils import venv_python
9
+ from .mode import EnvironmentMode
10
+
11
+ logger = logging.getLogger("module_runner")
12
+
13
+
14
+ def ensure_pip_environment(module_path: Path, module_name: str) -> Path:
15
+ """Provision and return the Python interpreter inside a venv."""
16
+ if importlib.util.find_spec("venv") is None:
17
+ detail = "pip mode requires the standard library venv module, but it is unavailable"
18
+ raise RuntimeError(
19
+ f"Environment '{EnvironmentMode.PIP.value}' is unavailable for module '{module_name}': {detail}"
20
+ )
21
+
22
+ venv_path = module_path / VENV_DIR
23
+ python_bin = venv_python(venv_path)
24
+
25
+ if not venv_path.exists():
26
+ create_cmd = [sys.executable, "-m", "venv", str(venv_path)]
27
+ logger.info("[%s] creating venv: %s", module_name, " ".join(create_cmd))
28
+ _run_checked(create_cmd, module_path, module_name, "create virtual environment")
29
+
30
+ pyproject = module_path / PYPROJECT_TOML
31
+ req = module_path / REQUIREMENTS_TXT
32
+
33
+ if pyproject.exists():
34
+ install_cmd = [str(python_bin), "-m", "pip", "install", "."]
35
+ logger.info("[%s] installing deps: %s", module_name, " ".join(install_cmd))
36
+ _run_checked(install_cmd, module_path, module_name, "install dependencies from pyproject.toml")
37
+ elif req.exists():
38
+ install_cmd = [str(python_bin), "-m", "pip", "install", "-r", str(req)]
39
+ logger.info("[%s] installing deps: %s", module_name, " ".join(install_cmd))
40
+ _run_checked(install_cmd, module_path, module_name, "install requirements")
41
+ else:
42
+ logger.info("[%s] reusing existing venv at %s", module_name, venv_path)
43
+
44
+ return python_bin
45
+
46
+
47
+ def _run_checked(cmd: list[str], module_path: Path, module_name: str, action: str) -> None:
48
+ try:
49
+ subprocess.run(
50
+ cmd,
51
+ check=True,
52
+ capture_output=True,
53
+ text=True,
54
+ cwd=str(module_path),
55
+ )
56
+ except subprocess.CalledProcessError as exc:
57
+ stderr = exc.stderr.strip() if exc.stderr else str(exc)
58
+ raise RuntimeError(
59
+ f"Environment '{EnvironmentMode.PIP.value}' failed to {action} for module '{module_name}': {stderr}"
60
+ ) from exc
@@ -0,0 +1,50 @@
1
+ import importlib.util
2
+ import logging
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from .constants import PYPROJECT_TOML, REQUIREMENTS_TXT
7
+ from .mode import EnvironmentMode
8
+
9
+ logger = logging.getLogger("module_runner")
10
+
11
+
12
+ def resolve_mode(module_path: Path, requested: EnvironmentMode, module_name: str) -> EnvironmentMode:
13
+ """Determine which environment mode should be used for the module."""
14
+ if requested is not EnvironmentMode.AUTO:
15
+ return requested
16
+
17
+ has_pyproject = (module_path / PYPROJECT_TOML).exists()
18
+ has_requirements = (module_path / REQUIREMENTS_TXT).exists()
19
+ uv_available = shutil.which("uv") is not None
20
+ pip_available = importlib.util.find_spec("venv") is not None
21
+
22
+ if has_pyproject:
23
+ if uv_available:
24
+ logger.info("[%s] strategy=uv (pyproject.toml detected, uv available)", module_name)
25
+ return EnvironmentMode.UV
26
+
27
+ if pip_available:
28
+ logger.info("[%s] strategy=pip (pyproject.toml detected, uv unavailable — fallback to venv+pip)", module_name)
29
+ return EnvironmentMode.PIP
30
+
31
+ detail = f"{PYPROJECT_TOML} detected but neither uv nor venv is available"
32
+ raise RuntimeError(
33
+ f"Environment setup failed for module '{module_name}': {detail}"
34
+ )
35
+
36
+ if has_requirements:
37
+ if uv_available:
38
+ logger.info("[%s] strategy=uv (requirements.txt detected, uv available)", module_name)
39
+ return EnvironmentMode.UV
40
+ if pip_available:
41
+ logger.info("[%s] strategy=pip (requirements.txt detected, uv unavailable — fallback to venv+pip)", module_name)
42
+ return EnvironmentMode.PIP
43
+
44
+ detail = f"{REQUIREMENTS_TXT} detected but pip is unavailable"
45
+ raise RuntimeError(
46
+ f"Environment '{EnvironmentMode.PIP.value}' is unavailable for module '{module_name}': {detail}"
47
+ )
48
+
49
+ logger.info("[%s] strategy=system (no dependency file found, using current interpreter)", module_name)
50
+ return EnvironmentMode.SYSTEM
@@ -0,0 +1,9 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+
5
+ def venv_python(venv_path: Path) -> Path:
6
+ """Return the Python interpreter path inside a virtual environment."""
7
+ if sys.platform.startswith("win"):
8
+ return venv_path / "Scripts" / "python.exe"
9
+ return venv_path / "bin" / "python"
@@ -0,0 +1,60 @@
1
+ import logging
2
+ import shutil
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from .constants import PYPROJECT_TOML, REQUIREMENTS_TXT, VENV_DIR
7
+ from .utils import venv_python
8
+ from .mode import EnvironmentMode
9
+
10
+ logger = logging.getLogger("module_runner")
11
+
12
+
13
+ def ensure_uv_environment(module_path: Path, module_name: str) -> Path:
14
+ """Ensure uv is available and provision dependencies in the module directory."""
15
+ uv_bin = shutil.which("uv")
16
+ if uv_bin is None:
17
+ raise RuntimeError(
18
+ f"Environment '{EnvironmentMode.UV.value}' is unavailable for module '{module_name}': uv executable not found in PATH"
19
+ )
20
+
21
+ requirements_path = module_path / REQUIREMENTS_TXT
22
+ pyproject_path = module_path / PYPROJECT_TOML
23
+
24
+ if requirements_path.exists() and not pyproject_path.exists():
25
+ venv_path = module_path / VENV_DIR
26
+ python_bin = venv_python(venv_path)
27
+ if not venv_path.exists():
28
+ create_cmd = [uv_bin, "venv"]
29
+ logger.info("[%s] creating venv: %s", module_name, " ".join(create_cmd))
30
+ _run_uv_checked(create_cmd, module_path, module_name, "create virtual environment")
31
+ else:
32
+ logger.info("[%s] reusing existing venv at %s", module_name, venv_path)
33
+ install_cmd = [uv_bin, "pip", "install", "-r", str(requirements_path)]
34
+ logger.info("[%s] installing deps: %s", module_name, " ".join(install_cmd))
35
+ _run_uv_checked(install_cmd, module_path, module_name, "install dependencies from requirements.txt")
36
+ return python_bin
37
+
38
+ logger.info("[%s] using uv run for pyproject.toml-based module", module_name)
39
+ return Path("uv")
40
+
41
+
42
+ def _run_uv_checked(
43
+ cmd: list[str],
44
+ module_path: Path,
45
+ module_name: str,
46
+ action: str,
47
+ ) -> None:
48
+ try:
49
+ subprocess.run(
50
+ cmd,
51
+ check=True,
52
+ capture_output=True,
53
+ text=True,
54
+ cwd=str(module_path),
55
+ )
56
+ except subprocess.CalledProcessError as exc:
57
+ stderr = exc.stderr.strip() if exc.stderr else str(exc)
58
+ raise RuntimeError(
59
+ f"Environment '{EnvironmentMode.UV.value}' failed to {action} for module '{module_name}': {stderr}"
60
+ ) from exc
File without changes
@@ -0,0 +1,33 @@
1
+ from typing import Optional
2
+
3
+
4
+ class RunnerExecutionError(Exception):
5
+ def __init__(
6
+ self,
7
+ module: str,
8
+ exit_code: int,
9
+ stderr: str,
10
+ stdout: str = "",
11
+ cmd: Optional[list[str]] = None,
12
+ ):
13
+ self.module = module
14
+ self.exit_code = exit_code
15
+ self.stderr = stderr
16
+ self.stdout = stdout
17
+ self.cmd = cmd
18
+
19
+ parts = [f"Module '{module}' failed with exit code {exit_code}"]
20
+
21
+ if cmd:
22
+ parts.append(f" command : {' '.join(cmd)}")
23
+
24
+ if stderr.strip():
25
+ parts.append(f" stderr : {stderr.strip()}")
26
+
27
+ if stdout.strip():
28
+ parts.append(f" stdout : {stdout.strip()}")
29
+
30
+ if not stderr.strip() and not stdout.strip():
31
+ parts.append(" (no output captured)")
32
+
33
+ super().__init__("\n".join(parts))
@@ -0,0 +1,60 @@
1
+ import json
2
+ import logging
3
+ import subprocess
4
+ from pathlib import Path
5
+ from subprocess import CompletedProcess
6
+ from typing import Optional
7
+
8
+ from .exceptions import RunnerExecutionError
9
+ from .environment import EnvironmentManager, EnvironmentMode
10
+ from .environment.constants import MAIN_PY
11
+
12
+ logger = logging.getLogger("module_runner")
13
+
14
+
15
+ class Runner:
16
+
17
+ def __init__(self, module_path: str | Path, environment: EnvironmentMode | str = EnvironmentMode.AUTO) -> None:
18
+ self.module_path = Path(module_path).resolve()
19
+ self.environment = environment
20
+ self.module_name = self.module_path.name
21
+
22
+ def run(
23
+ self,
24
+ payload: Optional[dict] = None,
25
+ ) -> CompletedProcess[str]:
26
+ script_path = self.module_path / MAIN_PY
27
+
28
+ if not script_path.exists():
29
+ raise FileNotFoundError(f"Module '{self.module_name}' does not contain {MAIN_PY}")
30
+
31
+ env_manager = EnvironmentManager(self.module_path, self.environment)
32
+ executor = env_manager.ensure()
33
+
34
+ if str(executor) == "uv":
35
+ cmd = ["uv", "run", "--project", str(self.module_path), "python", str(script_path)]
36
+ else:
37
+ cmd = [str(executor), str(script_path)]
38
+
39
+ if payload is not None:
40
+ cmd.append(json.dumps(payload))
41
+
42
+ logger.info("[%s] executing: %s", self.module_name, " ".join(cmd))
43
+
44
+ process: CompletedProcess[str] = subprocess.run(
45
+ cmd,
46
+ capture_output=True,
47
+ text=True,
48
+ cwd=str(self.module_path),
49
+ )
50
+
51
+ if process.returncode != 0:
52
+ raise RunnerExecutionError(
53
+ module=self.module_name,
54
+ exit_code=process.returncode,
55
+ stderr=process.stderr,
56
+ stdout=process.stdout,
57
+ cmd=cmd,
58
+ )
59
+
60
+ return process
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: module-runner
3
+ Version: 0.1.0
4
+ Summary: Lightweight helper that prepares Python environments and executes standalone modules.
5
+ Author: Leo Jaimesson
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # Module Runner
12
+
13
+ Module Runner sits in the gap between ad-hoc scripts and heavyweight orchestration frameworks. It gives each module a predictable, isolated environment without asking you to bolt on extra infrastructure, schedulers, or workflow engines. The project stays intentionally lightweight and focuses on two responsibilities:
14
+
15
+ 1. Detect the proper execution strategy (system Python, `venv`, or `uv`).
16
+ 2. Launch the module with an optional JSON payload, returning the `subprocess.CompletedProcess` so you can decide how to consume `stdout`, `stderr`, or exit codes.
17
+
18
+ Any higher-level orchestration (pipelines, RPC, shared storage, etc.) is an application concern, keeping this library small and predictable.
19
+
20
+ ## Features
21
+
22
+ - ⚙️ **Automatic environment discovery**: detects `pyproject.toml` or `requirements.txt` (→ `uv` when available, `venv` as fallback), or falls back to the current interpreter.
23
+ - 📦 **Per-module dependencies**: each module lives in its own folder with its own toolchain, avoiding cross-contamination.
24
+ - 🧱 **Opinionated boundary**: no implicit payload chaining—what a module prints, writes, or exposes is entirely up to you.
25
+ - 🪪 **Clear error surface**: `RunnerExecutionError` captures module failures while missing tooling raises descriptive `RuntimeError`s, keeping debugging straightforward.
26
+ - 🔍 **Built-in logging**: uses Python's standard `logging` module under the `module_runner` logger — enable `DEBUG` to see the resolved strategy, every setup command, and the final execution command.
27
+
28
+ ## Logging
29
+
30
+ Module Runner emits `INFO`-level log records under the `module_runner` logger. To see them:
31
+
32
+ ```python
33
+ import logging
34
+ logging.getLogger("module_runner").setLevel(logging.INFO)
35
+ logging.basicConfig()
36
+ ```
37
+
38
+ Example output:
39
+ ```
40
+ [normalize] strategy=pip (requirements.txt detected, uv unavailable — fallback to venv+pip)
41
+ [normalize] creating venv: /usr/bin/python3 -m venv .venv
42
+ [normalize] installing deps: .venv/bin/python -m pip install -r requirements.txt
43
+ [normalize] executing: .venv/bin/python main.py {"text": " Hello World "}
44
+ ```
45
+
46
+ ## Why This Exists
47
+
48
+ Module Runner is the missing middle layer between one-off helper scripts and enterprise orchestrators. It gives you just enough structure to keep automation tidy while remaining lightweight, infrastructure-free, and dependency-light.
49
+
50
+ ## When to Use
51
+
52
+ Use Module Runner when you:
53
+
54
+ - Need simple local automation or scripting glue with a lightweight footprint
55
+ - Have dependency conflicts between scripts and want per-module isolation
56
+ - Want predictable, sequential execution you can reason about
57
+ - Prefer to stay container-free for lightweight tasks
58
+ - Do not need a workflow engine or task scheduler
59
+
60
+ ## When Not to Use
61
+
62
+ Reach for other tooling if you require:
63
+
64
+ - Complex DAG dependencies or branching workflows
65
+ - Scheduling, cron-style orchestration, or SLAs
66
+ - Distributed workers or autoscaling fleets
67
+ - Monitoring dashboards, retries, or alerting UI
68
+ - Enterprise workflow/orchestration guarantees
69
+
70
+ ## Philosophy
71
+
72
+ - Small, explicit, predictable, and lightweight
73
+ - Infrastructure-free: relies on the Python already on your machine
74
+ - Focused on one problem—launching modules in clean environments
75
+
76
+ It is not meant to be a workflow engine; it simply keeps isolated scripts manageable.
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ # using pip
82
+ pip install module_runner
83
+
84
+ # using uv
85
+ uv add module_runner
86
+
87
+ ```
88
+
89
+ You only need Python 3.9+ and whichever tooling your modules request (e.g., `uv`, `venv`, system packages).
90
+
91
+ ## Quick Start
92
+
93
+ ```python
94
+ from module_runner import Runner
95
+
96
+ runner = Runner(module_path="modules/normalize_text")
97
+
98
+ process = runner.run(
99
+ payload={"text": " Hello World "},
100
+ )
101
+
102
+ print(process.stdout) # or json.loads(process.stdout)
103
+ ```
104
+
105
+ Your module layout needs a `main.py` entry point. Any payload you pass is serialized as JSON and delivered as the last CLI argument. How you emit results (stdout, files, sockets) is entirely your call.
106
+
107
+ ## Environment Selection
108
+
109
+ | Signal in module folder | Mode used | Requirement |
110
+ | ----------------------- | ------------- | ----------- |
111
+ | `pyproject.toml` | `uv run ...` | `uv` available in `PATH` |
112
+ | `pyproject.toml` (no `uv`) | `python -m venv` + `pip install .` | `venv` module available |
113
+ | `requirements.txt` | `uv venv` + `uv pip install -r requirements.txt` | `uv` available in `PATH` |
114
+ | `requirements.txt` (no `uv`) | `python -m venv` + `pip install -r requirements.txt` | `venv` module available |
115
+ | none of the above | current interpreter (`sys.executable`) | none |
116
+
117
+ - Auto-detection always checks whether the required tooling is installed. When `uv` is available it is preferred for both `pyproject.toml` and `requirements.txt` modules. If `uv` is not available, both `pyproject.toml` and `requirements.txt` modules fall back to `venv`/`pip`.
118
+ - If a module signals that it needs `uv` or `venv` but the corresponding tooling is missing, Module Runner raises a descriptive `RuntimeError` explaining what needs to be installed.
119
+ - Runtime failures propagate as `RunnerExecutionError`, exposing the module name, exit code, captured `stderr`, captured `stdout`, and the exact `cmd` list that was executed — making it straightforward to reproduce or log failures.
120
+
121
+ ## Sandbox Playground
122
+
123
+ The [sandbox](sandbox/README.md) directory ships with two toy modules (`normalize` and `stats`) that showcase:
124
+
125
+ - How `requirements.txt` triggers a module-specific virtual environment.
126
+ - How `pyproject.toml` causes execution via `uv run`.
127
+ - How you can manually chain modules by parsing the `stdout` from the first run and feeding it into the next.
128
+
129
+ Run everything with:
130
+
131
+ ```bash
132
+ python sandbox/run_examples.py
133
+ ```
134
+
135
+ Use this folder as a template to create your own modules or to test different deployment scenarios.
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ git clone https://github.com/<seu-usuario>/module_runner.git
141
+ cd module_runner
142
+ python -m venv .venv && source .venv/bin/activate
143
+ pip install -e .
144
+ ```
145
+
146
+ Feel free to open issues or pull requests with improvements. The scope intentionally stays small: reliable environment setup and process execution.
147
+
148
+ ## License
149
+
150
+ Module Runner is released under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ module_runner/__init__.py
5
+ module_runner/environments.py
6
+ module_runner/exceptions.py
7
+ module_runner/runner.py
8
+ module_runner.egg-info/PKG-INFO
9
+ module_runner.egg-info/SOURCES.txt
10
+ module_runner.egg-info/dependency_links.txt
11
+ module_runner.egg-info/top_level.txt
12
+ module_runner/environment/__init__.py
13
+ module_runner/environment/constants.py
14
+ module_runner/environment/manager.py
15
+ module_runner/environment/mode.py
16
+ module_runner/environment/pip_env.py
17
+ module_runner/environment/resolver.py
18
+ module_runner/environment/utils.py
19
+ module_runner/environment/uv_env.py
@@ -0,0 +1 @@
1
+ module_runner
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "module-runner"
3
+ version = "0.1.0"
4
+ description = "Lightweight helper that prepares Python environments and executes standalone modules."
5
+ authors = [{name = "Leo Jaimesson"}]
6
+ readme = "README.md"
7
+ requires-python = ">=3.10"
8
+ dependencies = []
9
+
10
+ [tool.setuptools.packages.find]
11
+ include = ["module_runner*"]
12
+
13
+ [build-system]
14
+ requires = ["setuptools"]
15
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+