local-dev-composer 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.
Files changed (62) hide show
  1. local_dev_composer-0.1.0/PKG-INFO +88 -0
  2. local_dev_composer-0.1.0/README.md +59 -0
  3. local_dev_composer-0.1.0/pyproject.toml +60 -0
  4. local_dev_composer-0.1.0/setup.cfg +4 -0
  5. local_dev_composer-0.1.0/src/ldc/__init__.py +3 -0
  6. local_dev_composer-0.1.0/src/ldc/__main__.py +5 -0
  7. local_dev_composer-0.1.0/src/ldc/adapters/__init__.py +0 -0
  8. local_dev_composer-0.1.0/src/ldc/adapters/config/__init__.py +0 -0
  9. local_dev_composer-0.1.0/src/ldc/adapters/config/yaml_reader.py +172 -0
  10. local_dev_composer-0.1.0/src/ldc/adapters/git/__init__.py +0 -0
  11. local_dev_composer-0.1.0/src/ldc/adapters/git/subprocess_client.py +64 -0
  12. local_dev_composer-0.1.0/src/ldc/adapters/health/__init__.py +0 -0
  13. local_dev_composer-0.1.0/src/ldc/adapters/health/command_checker.py +41 -0
  14. local_dev_composer-0.1.0/src/ldc/adapters/health/composite_checker.py +39 -0
  15. local_dev_composer-0.1.0/src/ldc/adapters/health/http_checker.py +31 -0
  16. local_dev_composer-0.1.0/src/ldc/adapters/health/process_checker.py +28 -0
  17. local_dev_composer-0.1.0/src/ldc/adapters/health/tcp_checker.py +33 -0
  18. local_dev_composer-0.1.0/src/ldc/adapters/prerequisites/__init__.py +0 -0
  19. local_dev_composer-0.1.0/src/ldc/adapters/prerequisites/system_checker.py +256 -0
  20. local_dev_composer-0.1.0/src/ldc/adapters/process/__init__.py +0 -0
  21. local_dev_composer-0.1.0/src/ldc/adapters/process/windows_runner.py +143 -0
  22. local_dev_composer-0.1.0/src/ldc/adapters/reporting/__init__.py +0 -0
  23. local_dev_composer-0.1.0/src/ldc/adapters/reporting/rich_reporter.py +211 -0
  24. local_dev_composer-0.1.0/src/ldc/adapters/state/__init__.py +0 -0
  25. local_dev_composer-0.1.0/src/ldc/adapters/state/json_store.py +64 -0
  26. local_dev_composer-0.1.0/src/ldc/application/__init__.py +1 -0
  27. local_dev_composer-0.1.0/src/ldc/application/commands/__init__.py +0 -0
  28. local_dev_composer-0.1.0/src/ldc/application/commands/bootstrap.py +91 -0
  29. local_dev_composer-0.1.0/src/ldc/application/commands/check.py +65 -0
  30. local_dev_composer-0.1.0/src/ldc/application/commands/clone.py +77 -0
  31. local_dev_composer-0.1.0/src/ldc/application/commands/doctor.py +115 -0
  32. local_dev_composer-0.1.0/src/ldc/application/commands/down.py +61 -0
  33. local_dev_composer-0.1.0/src/ldc/application/commands/env.py +94 -0
  34. local_dev_composer-0.1.0/src/ldc/application/commands/install.py +61 -0
  35. local_dev_composer-0.1.0/src/ldc/application/commands/logs.py +74 -0
  36. local_dev_composer-0.1.0/src/ldc/application/commands/rebuild.py +86 -0
  37. local_dev_composer-0.1.0/src/ldc/application/commands/restart.py +65 -0
  38. local_dev_composer-0.1.0/src/ldc/application/commands/status.py +36 -0
  39. local_dev_composer-0.1.0/src/ldc/application/commands/up.py +229 -0
  40. local_dev_composer-0.1.0/src/ldc/application/container.py +83 -0
  41. local_dev_composer-0.1.0/src/ldc/application/env_resolver.py +61 -0
  42. local_dev_composer-0.1.0/src/ldc/application/installer_service.py +41 -0
  43. local_dev_composer-0.1.0/src/ldc/cli.py +242 -0
  44. local_dev_composer-0.1.0/src/ldc/domain/__init__.py +1 -0
  45. local_dev_composer-0.1.0/src/ldc/domain/exceptions.py +29 -0
  46. local_dev_composer-0.1.0/src/ldc/domain/graph.py +149 -0
  47. local_dev_composer-0.1.0/src/ldc/domain/models.py +172 -0
  48. local_dev_composer-0.1.0/src/ldc/ports/__init__.py +1 -0
  49. local_dev_composer-0.1.0/src/ldc/ports/config_reader.py +11 -0
  50. local_dev_composer-0.1.0/src/ldc/ports/git_client.py +22 -0
  51. local_dev_composer-0.1.0/src/ldc/ports/health_checker.py +28 -0
  52. local_dev_composer-0.1.0/src/ldc/ports/installer.py +24 -0
  53. local_dev_composer-0.1.0/src/ldc/ports/prerequisite_checker.py +23 -0
  54. local_dev_composer-0.1.0/src/ldc/ports/process_runner.py +39 -0
  55. local_dev_composer-0.1.0/src/ldc/ports/reporter.py +43 -0
  56. local_dev_composer-0.1.0/src/ldc/ports/state_store.py +20 -0
  57. local_dev_composer-0.1.0/src/local_dev_composer.egg-info/PKG-INFO +88 -0
  58. local_dev_composer-0.1.0/src/local_dev_composer.egg-info/SOURCES.txt +60 -0
  59. local_dev_composer-0.1.0/src/local_dev_composer.egg-info/dependency_links.txt +1 -0
  60. local_dev_composer-0.1.0/src/local_dev_composer.egg-info/entry_points.txt +2 -0
  61. local_dev_composer-0.1.0/src/local_dev_composer.egg-info/requires.txt +7 -0
  62. local_dev_composer-0.1.0/src/local_dev_composer.egg-info/top_level.txt +1 -0
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: local-dev-composer
3
+ Version: 0.1.0
4
+ Summary: Orchestrate local microservice environments on Windows without Docker or WSL
5
+ Author: Sergii Bugera
6
+ Project-URL: Homepage, https://github.com/sbugera/local-dev-composer
7
+ Project-URL: Repository, https://github.com/sbugera/local-dev-composer
8
+ Project-URL: Issues, https://github.com/sbugera/local-dev-composer/issues
9
+ Keywords: microservices,local-development,developer-tools,process-manager,windows,devtools
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Build Tools
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: rich>=13.0.0
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: psutil>=5.9.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
29
+
30
+ # local-dev-composer (ldc)
31
+
32
+ Orchestrate local microservice environments on **Windows 11** without Docker, Podman, or WSL.
33
+
34
+ - Start a full service graph with one command
35
+ - Per-service environment isolation (same variable, different values per service)
36
+ - Dependency ordering — services start leaf-first, stop dependents-first
37
+ - Prerequisite checks with actionable fix hints (Java version, PATH commands, env vars, ports, folders)
38
+ - Live Rich terminal dashboard
39
+ - Groups — declare the minimum set of services per development scenario
40
+ - Survives restarts — reconciles live PIDs from `.ldc/state.json`
41
+
42
+ ## Quick start
43
+
44
+ ```bash
45
+ git clone https://github.com/sbugera/local-dev-composer.git
46
+ cd local-dev-composer
47
+ pip install -e .
48
+
49
+ cp composer.example.yml composer.yml
50
+ # edit composer.yml for your project
51
+
52
+ ldc doctor # check everything, get a numbered fix list
53
+ ldc clone # clone all repos
54
+ ldc check --fix # verify and fix prerequisites
55
+ ldc install # run install commands
56
+ ldc up --group gateway-dev # start minimum services for your task
57
+ ```
58
+
59
+ ## Daily use
60
+
61
+ ```bash
62
+ ldc up --group gateway-dev # start what you need
63
+ ldc status # service table with PIDs and health
64
+ ldc logs gateway -f # follow a service log
65
+ ldc down # stop everything
66
+ ```
67
+
68
+ ## Documentation
69
+
70
+ | topic | description |
71
+ |-------|-------------|
72
+ | [Installation](docs/installation.md) | Setup, direct-script mode |
73
+ | [Configuration](docs/configuration.md) | Full `composer.yml` schema reference |
74
+ | [Commands](docs/commands.md) | All CLI commands with options |
75
+ | [Groups](docs/groups.md) | Named service sets, smart selection |
76
+ | [Environment](docs/environment.md) | Per-service env isolation, `.env` files |
77
+ | [Prerequisites](docs/prerequisites.md) | Runtime/command/folder/port checks |
78
+ | [Health Checks](docs/health-checks.md) | HTTP, TCP, command, process types |
79
+ | [State & Logs](docs/state-and-logs.md) | `.ldc/state.json`, log files, clearing state |
80
+ | [Architecture](docs/architecture.md) | Hexagonal design, layers, extending ldc |
81
+ | [Testing](docs/testing.md) | Running tests, writing new ones |
82
+
83
+ ## Requirements
84
+
85
+ - Python 3.9+
86
+ - Git for Windows
87
+ - Windows 11
88
+ - No Docker, no WSL
@@ -0,0 +1,59 @@
1
+ # local-dev-composer (ldc)
2
+
3
+ Orchestrate local microservice environments on **Windows 11** without Docker, Podman, or WSL.
4
+
5
+ - Start a full service graph with one command
6
+ - Per-service environment isolation (same variable, different values per service)
7
+ - Dependency ordering — services start leaf-first, stop dependents-first
8
+ - Prerequisite checks with actionable fix hints (Java version, PATH commands, env vars, ports, folders)
9
+ - Live Rich terminal dashboard
10
+ - Groups — declare the minimum set of services per development scenario
11
+ - Survives restarts — reconciles live PIDs from `.ldc/state.json`
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ git clone https://github.com/sbugera/local-dev-composer.git
17
+ cd local-dev-composer
18
+ pip install -e .
19
+
20
+ cp composer.example.yml composer.yml
21
+ # edit composer.yml for your project
22
+
23
+ ldc doctor # check everything, get a numbered fix list
24
+ ldc clone # clone all repos
25
+ ldc check --fix # verify and fix prerequisites
26
+ ldc install # run install commands
27
+ ldc up --group gateway-dev # start minimum services for your task
28
+ ```
29
+
30
+ ## Daily use
31
+
32
+ ```bash
33
+ ldc up --group gateway-dev # start what you need
34
+ ldc status # service table with PIDs and health
35
+ ldc logs gateway -f # follow a service log
36
+ ldc down # stop everything
37
+ ```
38
+
39
+ ## Documentation
40
+
41
+ | topic | description |
42
+ |-------|-------------|
43
+ | [Installation](docs/installation.md) | Setup, direct-script mode |
44
+ | [Configuration](docs/configuration.md) | Full `composer.yml` schema reference |
45
+ | [Commands](docs/commands.md) | All CLI commands with options |
46
+ | [Groups](docs/groups.md) | Named service sets, smart selection |
47
+ | [Environment](docs/environment.md) | Per-service env isolation, `.env` files |
48
+ | [Prerequisites](docs/prerequisites.md) | Runtime/command/folder/port checks |
49
+ | [Health Checks](docs/health-checks.md) | HTTP, TCP, command, process types |
50
+ | [State & Logs](docs/state-and-logs.md) | `.ldc/state.json`, log files, clearing state |
51
+ | [Architecture](docs/architecture.md) | Hexagonal design, layers, extending ldc |
52
+ | [Testing](docs/testing.md) | Running tests, writing new ones |
53
+
54
+ ## Requirements
55
+
56
+ - Python 3.9+
57
+ - Git for Windows
58
+ - Windows 11
59
+ - No Docker, no WSL
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "local-dev-composer"
7
+ version = "0.1.0"
8
+ description = "Orchestrate local microservice environments on Windows without Docker or WSL"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [
12
+ {name = "Sergii Bugera"},
13
+ ]
14
+ keywords = [
15
+ "microservices", "local-development", "developer-tools",
16
+ "process-manager", "windows", "devtools",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Environment :: Console",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: Microsoft :: Windows",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Topic :: Software Development :: Build Tools",
29
+ "Topic :: System :: Systems Administration",
30
+ ]
31
+ dependencies = [
32
+ "rich>=13.0.0",
33
+ "pyyaml>=6.0",
34
+ "psutil>=5.9.0",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/sbugera/local-dev-composer"
39
+ Repository = "https://github.com/sbugera/local-dev-composer"
40
+ Issues = "https://github.com/sbugera/local-dev-composer/issues"
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "pytest>=7.0",
45
+ "pytest-cov>=4.0",
46
+ ]
47
+
48
+ [project.scripts]
49
+ ldc = "ldc.cli:main"
50
+
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ pythonpath = ["src"]
57
+ addopts = "--import-mode=importlib"
58
+
59
+ [tool.coverage.run]
60
+ source = ["src/ldc"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """local-dev-composer (ldc) — orchestrate local microservice environments."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Enables python -m ldc invocation."""
2
+ from ldc.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
File without changes
@@ -0,0 +1,172 @@
1
+ """
2
+ Adapter: reads a composer.yml file and produces a WorkspaceConfig.
3
+
4
+ YAML schema is deliberately flat and human-friendly; this adapter maps it
5
+ to the rich domain model.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+ import yaml
13
+
14
+ from ldc.domain.exceptions import ConfigValidationError
15
+ from ldc.domain.models import (
16
+ Group,
17
+ HealthCheckConfig,
18
+ HealthCheckType,
19
+ InstallConfig,
20
+ Prerequisites,
21
+ Runtime,
22
+ Service,
23
+ StartConfig,
24
+ WorkspaceConfig,
25
+ )
26
+ from ldc.ports.config_reader import IConfigReader
27
+
28
+
29
+ class YamlConfigReader(IConfigReader):
30
+
31
+ def read(self, path: Path) -> WorkspaceConfig:
32
+ if not path.exists():
33
+ raise ConfigValidationError(f"Config file not found: {path}")
34
+
35
+ with path.open("r", encoding="utf-8") as fh:
36
+ raw = yaml.safe_load(fh)
37
+
38
+ if not isinstance(raw, dict):
39
+ raise ConfigValidationError("composer.yml must be a YAML mapping")
40
+
41
+ workspace_raw = raw.get("workspace", {})
42
+ root = workspace_raw.get("root", "./services")
43
+ log_dir = workspace_raw.get("log_dir", "./logs")
44
+
45
+ services_raw: Dict[str, Any] = raw.get("services", {})
46
+ groups_raw: Dict[str, Any] = raw.get("groups", {})
47
+
48
+ services = {
49
+ name: self._parse_service(name, svc_raw)
50
+ for name, svc_raw in services_raw.items()
51
+ }
52
+
53
+ groups = {
54
+ name: self._parse_group(name, grp_raw)
55
+ for name, grp_raw in groups_raw.items()
56
+ }
57
+
58
+ return WorkspaceConfig(
59
+ root=root,
60
+ log_dir=log_dir,
61
+ services=services,
62
+ groups=groups,
63
+ )
64
+
65
+ # ------------------------------------------------------------------
66
+ # Private helpers
67
+ # ------------------------------------------------------------------
68
+
69
+ def _parse_service(self, name: str, raw: Dict[str, Any]) -> Service:
70
+ runtime_str = raw.get("runtime", "custom")
71
+ try:
72
+ runtime = Runtime(runtime_str.lower())
73
+ except ValueError:
74
+ raise ConfigValidationError(
75
+ f"Service '{name}': unknown runtime '{runtime_str}'. "
76
+ f"Valid values: {[r.value for r in Runtime]}"
77
+ )
78
+
79
+ return Service(
80
+ name=name,
81
+ runtime=runtime,
82
+ depends_on=raw.get("depends_on", []),
83
+ repo=raw.get("repo"),
84
+ branch=raw.get("branch", "main"),
85
+ dir=raw.get("dir"),
86
+ env=raw.get("env", {}),
87
+ env_files=self._parse_env_files(raw),
88
+ requires=self._parse_prerequisites(raw.get("requires")),
89
+ install=self._parse_install(raw.get("install")),
90
+ start=self._parse_start(raw.get("start")),
91
+ health_check=self._parse_health_check(name, raw.get("health_check")),
92
+ labels=raw.get("labels", {}),
93
+ description=raw.get("description", ""),
94
+ )
95
+
96
+ def _parse_env_files(self, raw: Dict[str, Any]) -> list:
97
+ if "env_files" in raw:
98
+ value = raw["env_files"]
99
+ return [value] if isinstance(value, str) else list(value)
100
+ if "env_file" in raw:
101
+ value = raw["env_file"]
102
+ return [value] if value else []
103
+ return []
104
+
105
+ def _parse_prerequisites(self, raw: Optional[Dict]) -> Optional[Prerequisites]:
106
+ if not raw:
107
+ return None
108
+ return Prerequisites(
109
+ java=raw.get("java"),
110
+ python=raw.get("python"),
111
+ node=raw.get("node"),
112
+ dotnet=raw.get("dotnet"),
113
+ commands=raw.get("commands", []),
114
+ env_vars=raw.get("env_vars", []),
115
+ folders=raw.get("folders", []),
116
+ files=raw.get("files", []),
117
+ ports_free=raw.get("ports_free", []),
118
+ )
119
+
120
+ def _parse_install(self, raw: Optional[Any]) -> Optional[InstallConfig]:
121
+ if not raw:
122
+ return None
123
+ if isinstance(raw, str):
124
+ return InstallConfig(command=raw)
125
+ return InstallConfig(
126
+ command=raw["command"],
127
+ working_dir=raw.get("working_dir", "."),
128
+ )
129
+
130
+ def _parse_start(self, raw: Optional[Any]) -> Optional[StartConfig]:
131
+ if not raw:
132
+ return None
133
+ if isinstance(raw, str):
134
+ return StartConfig(command=raw)
135
+ return StartConfig(
136
+ command=raw["command"],
137
+ args=raw.get("args", []),
138
+ working_dir=raw.get("working_dir", "."),
139
+ )
140
+
141
+ def _parse_health_check(
142
+ self, service_name: str, raw: Optional[Dict]
143
+ ) -> Optional[HealthCheckConfig]:
144
+ if not raw:
145
+ return None
146
+
147
+ type_str = raw.get("type", "process")
148
+ try:
149
+ hc_type = HealthCheckType(type_str.lower())
150
+ except ValueError:
151
+ raise ConfigValidationError(
152
+ f"Service '{service_name}': unknown health_check type '{type_str}'"
153
+ )
154
+
155
+ return HealthCheckConfig(
156
+ type=hc_type,
157
+ url=raw.get("url"),
158
+ host=raw.get("host", "localhost"),
159
+ port=raw.get("port"),
160
+ command=raw.get("command"),
161
+ expected_output=raw.get("expected_output"),
162
+ interval_seconds=raw.get("interval", 5),
163
+ timeout_seconds=raw.get("timeout", 60),
164
+ retries=raw.get("retries", 12),
165
+ )
166
+
167
+ def _parse_group(self, name: str, raw: Dict[str, Any]) -> Group:
168
+ return Group(
169
+ name=name,
170
+ description=raw.get("description", ""),
171
+ services=raw.get("services", []),
172
+ )
@@ -0,0 +1,64 @@
1
+ """
2
+ Adapter: git operations via the system git binary (via subprocess).
3
+ Works on Windows with Git for Windows / GitBash.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ from ldc.ports.git_client import IGitClient
11
+
12
+
13
+ class SubprocessGitClient(IGitClient):
14
+
15
+ def clone(self, repo_url: str, dest: Path, branch: str = "main") -> None:
16
+ dest.parent.mkdir(parents=True, exist_ok=True)
17
+ self._run(
18
+ ["git", "clone", "--branch", branch, "--depth", "1", repo_url, dest.name],
19
+ cwd=str(dest.parent),
20
+ )
21
+
22
+ def pull(self, repo_dir: Path, branch: str = "main") -> None:
23
+ self._run(["git", "fetch", "origin"], cwd=str(repo_dir))
24
+ self._run(
25
+ ["git", "checkout", branch], cwd=str(repo_dir)
26
+ )
27
+ self._run(
28
+ ["git", "pull", "origin", branch, "--ff-only"],
29
+ cwd=str(repo_dir),
30
+ )
31
+
32
+ def is_cloned(self, dest: Path) -> bool:
33
+ git_dir = dest / ".git"
34
+ return git_dir.exists()
35
+
36
+ def current_branch(self, repo_dir: Path) -> str:
37
+ result = self._run(
38
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
39
+ cwd=str(repo_dir),
40
+ capture=True,
41
+ )
42
+ return result.stdout.strip()
43
+
44
+ # ------------------------------------------------------------------
45
+
46
+ def _run(
47
+ self,
48
+ cmd: list,
49
+ cwd: str,
50
+ capture: bool = False,
51
+ ) -> subprocess.CompletedProcess:
52
+ result = subprocess.run(
53
+ cmd,
54
+ cwd=cwd,
55
+ capture_output=capture,
56
+ text=True,
57
+ check=False,
58
+ )
59
+ if result.returncode != 0:
60
+ stderr = result.stderr.strip() if capture else ""
61
+ raise RuntimeError(
62
+ f"git command failed (exit {result.returncode}): {' '.join(cmd)}\n{stderr}"
63
+ )
64
+ return result
@@ -0,0 +1,41 @@
1
+ """Adapter: health check via arbitrary command exit code / output."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import time
6
+
7
+ from ldc.domain.models import HealthCheckConfig, HealthCheckType
8
+ from ldc.ports.health_checker import IHealthChecker
9
+
10
+
11
+ class CommandHealthChecker(IHealthChecker):
12
+
13
+ def supports(self, config: HealthCheckConfig) -> bool:
14
+ return config.type == HealthCheckType.COMMAND
15
+
16
+ def check(self, config: HealthCheckConfig) -> bool:
17
+ if not config.command:
18
+ return False
19
+ try:
20
+ result = subprocess.run(
21
+ config.command,
22
+ shell=True,
23
+ capture_output=True,
24
+ text=True,
25
+ timeout=10,
26
+ )
27
+ if result.returncode != 0:
28
+ return False
29
+ if config.expected_output:
30
+ return config.expected_output in (result.stdout + result.stderr)
31
+ return True
32
+ except Exception:
33
+ return False
34
+
35
+ def wait_healthy(self, config: HealthCheckConfig) -> bool:
36
+ deadline = time.monotonic() + config.timeout_seconds
37
+ while time.monotonic() < deadline:
38
+ if self.check(config):
39
+ return True
40
+ time.sleep(config.interval_seconds)
41
+ return False
@@ -0,0 +1,39 @@
1
+ """
2
+ Adapter: dispatches health checks to the appropriate concrete checker
3
+ based on HealthCheckConfig.type.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import time
8
+ from typing import List
9
+
10
+ from ldc.domain.models import HealthCheckConfig
11
+ from ldc.ports.health_checker import IHealthChecker
12
+
13
+
14
+ class CompositeHealthChecker(IHealthChecker):
15
+ """Routes to the first registered checker that supports the config type."""
16
+
17
+ def __init__(self, checkers: List[IHealthChecker]) -> None:
18
+ self._checkers = checkers
19
+
20
+ def supports(self, config: HealthCheckConfig) -> bool:
21
+ return any(c.supports(config) for c in self._checkers)
22
+
23
+ def check(self, config: HealthCheckConfig) -> bool:
24
+ checker = self._find(config)
25
+ if checker is None:
26
+ return False
27
+ return checker.check(config)
28
+
29
+ def wait_healthy(self, config: HealthCheckConfig) -> bool:
30
+ checker = self._find(config)
31
+ if checker is None:
32
+ return False
33
+ return checker.wait_healthy(config)
34
+
35
+ def _find(self, config: HealthCheckConfig) -> "IHealthChecker | None":
36
+ for c in self._checkers:
37
+ if c.supports(config):
38
+ return c
39
+ return None
@@ -0,0 +1,31 @@
1
+ """Adapter: HTTP/HTTPS health check."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ import urllib.request
6
+ import urllib.error
7
+
8
+ from ldc.domain.models import HealthCheckConfig, HealthCheckType
9
+ from ldc.ports.health_checker import IHealthChecker
10
+
11
+
12
+ class HttpHealthChecker(IHealthChecker):
13
+
14
+ def supports(self, config: HealthCheckConfig) -> bool:
15
+ return config.type == HealthCheckType.HTTP
16
+
17
+ def check(self, config: HealthCheckConfig) -> bool:
18
+ try:
19
+ req = urllib.request.Request(config.url)
20
+ with urllib.request.urlopen(req, timeout=5) as resp:
21
+ return 200 <= resp.status < 400
22
+ except Exception:
23
+ return False
24
+
25
+ def wait_healthy(self, config: HealthCheckConfig) -> bool:
26
+ deadline = time.monotonic() + config.timeout_seconds
27
+ while time.monotonic() < deadline:
28
+ if self.check(config):
29
+ return True
30
+ time.sleep(config.interval_seconds)
31
+ return False
@@ -0,0 +1,28 @@
1
+ """Adapter: health check that simply verifies the process is alive."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+
6
+ import psutil
7
+
8
+ from ldc.domain.models import HealthCheckConfig, HealthCheckType
9
+ from ldc.ports.health_checker import IHealthChecker
10
+
11
+
12
+ class ProcessHealthChecker(IHealthChecker):
13
+
14
+ def __init__(self, pid_provider) -> None:
15
+ # pid_provider: callable(service_name) -> Optional[int]
16
+ self._pid_provider = pid_provider
17
+
18
+ def supports(self, config: HealthCheckConfig) -> bool:
19
+ return config.type == HealthCheckType.PROCESS
20
+
21
+ def check(self, config: HealthCheckConfig) -> bool:
22
+ # For PROCESS type we just need any live process — the caller supplies PID
23
+ # via the provider; config carries no PID directly.
24
+ # This checker is used as a fallback by the orchestrator passing PID explicitly.
25
+ return True # orchestrator checks is_alive() directly for PROCESS type
26
+
27
+ def wait_healthy(self, config: HealthCheckConfig) -> bool:
28
+ return True
@@ -0,0 +1,33 @@
1
+ """Adapter: TCP port health check."""
2
+ from __future__ import annotations
3
+
4
+ import socket
5
+ import time
6
+
7
+ from ldc.domain.models import HealthCheckConfig, HealthCheckType
8
+ from ldc.ports.health_checker import IHealthChecker
9
+
10
+
11
+ class TcpHealthChecker(IHealthChecker):
12
+
13
+ def supports(self, config: HealthCheckConfig) -> bool:
14
+ return config.type == HealthCheckType.TCP
15
+
16
+ def check(self, config: HealthCheckConfig) -> bool:
17
+ host = config.host or "localhost"
18
+ port = config.port
19
+ if not port:
20
+ return False
21
+ try:
22
+ with socket.create_connection((host, port), timeout=3):
23
+ return True
24
+ except (OSError, socket.timeout):
25
+ return False
26
+
27
+ def wait_healthy(self, config: HealthCheckConfig) -> bool:
28
+ deadline = time.monotonic() + config.timeout_seconds
29
+ while time.monotonic() < deadline:
30
+ if self.check(config):
31
+ return True
32
+ time.sleep(config.interval_seconds)
33
+ return False