Add initial implementation of Docker Compose manager and example usage
- Introduced DockerComposeManager class for programmatically creating and managing Docker Compose YAML files. - Added example script demonstrating usage of DockerComposeManager. - Created tests for service creation, modification, and removal. - Included project metadata in pyproject.toml and added linting instructions in copilot-instructions.md.
This commit is contained in:
7
.github/copilot-instructions.md
vendored
Normal file
7
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
This Python library is used to create Docker compose.yaml files from Python classes.
|
||||
|
||||
It is designed to simplify the process of defining and managing Docker Compose configurations programmatically.
|
||||
|
||||
Uses uv for Python dependency management. `uv sync`, `uv add <package>`.
|
||||
|
||||
Ruff is used for linting and formatting. Use `ruff check .` to check the code and `ruff format .` to format it.
|
0
example/__init__.py
Normal file
0
example/__init__.py
Normal file
36
example/basic_example.py
Normal file
36
example/basic_example.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Example usage of DockerComposeManager to generate a docker-compose.yaml file."""
|
||||
|
||||
from compose import DockerComposeManager
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Path to the compose file to generate
|
||||
compose_path = "docker-compose.yaml"
|
||||
|
||||
# Create a DockerComposeManager instance
|
||||
manager = DockerComposeManager(compose_path)
|
||||
|
||||
# Add a simple web service
|
||||
manager.create_service(
|
||||
name="web",
|
||||
image="nginx:alpine",
|
||||
ports=["8080:80"],
|
||||
environment={"NGINX_HOST": "localhost"},
|
||||
)
|
||||
|
||||
# Add a database service
|
||||
manager.create_service(
|
||||
name="db",
|
||||
image="postgres:15-alpine",
|
||||
environment={
|
||||
"POSTGRES_USER": "user",
|
||||
"POSTGRES_PASSWORD": "password",
|
||||
"POSTGRES_DB": "exampledb",
|
||||
},
|
||||
ports=["5432:5432"],
|
||||
volumes=["db_data:/var/lib/postgresql/data"],
|
||||
)
|
||||
|
||||
# Save the compose file
|
||||
manager.save()
|
||||
|
||||
print(f"docker-compose.yaml generated at {compose_path}")
|
14
pyproject.toml
Normal file
14
pyproject.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "compose"
|
||||
version = "0.1.0"
|
||||
description = "A simple Python package for managing Docker Compose files"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"pytest>=8.4.0",
|
||||
"pyyaml>=6.0.2",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
133
src/compose/__init__.py
Normal file
133
src/compose/__init__.py
Normal file
@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2023, the Compose project contributors.
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
"""Docker Compose YAML file generator.
|
||||
|
||||
This package provides utilities for programmatically creating and managing Docker Compose
|
||||
configuration files through Python classes, simplifying the process of defining
|
||||
and managing Docker Compose configurations.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class DockerComposeManager:
|
||||
"""A class to create and modify Docker Compose YAML files programmatically.
|
||||
|
||||
Supports context manager usage for auto-saving.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, version: str = "3.8") -> None:
|
||||
"""Initialize the manager with a YAML file path. Loads existing file or creates a new one."""
|
||||
self.path: str = path
|
||||
self.version: str = version
|
||||
self._data: dict[str, Any] = {}
|
||||
self._dirty: bool = False
|
||||
self._load()
|
||||
|
||||
def _load(self) -> None:
|
||||
if Path(self.path).exists():
|
||||
with Path(self.path).open("r", encoding="utf-8") as f:
|
||||
self._data = yaml.safe_load(f) or {}
|
||||
if not self._data:
|
||||
self._data = {"version": self.version, "services": {}}
|
||||
if "services" not in self._data:
|
||||
self._data["services"] = {}
|
||||
if not isinstance(self._data["services"], dict):
|
||||
self._data["services"] = {}
|
||||
|
||||
def create_service(
|
||||
self,
|
||||
name: str,
|
||||
image: str,
|
||||
ports: list[str] | None = None,
|
||||
environment: dict[str, str] | None = None,
|
||||
**kwargs: object,
|
||||
) -> "DockerComposeManager":
|
||||
"""Create a new service in the compose file.
|
||||
|
||||
Returns:
|
||||
DockerComposeManager: self (for chaining)
|
||||
|
||||
"""
|
||||
services = self._data["services"]
|
||||
service: dict[str, Any] = {"image": image}
|
||||
if ports is not None:
|
||||
service["ports"] = ports
|
||||
if environment is not None:
|
||||
service["environment"] = environment
|
||||
service.update(kwargs)
|
||||
services[name] = service
|
||||
self._dirty = True
|
||||
return self
|
||||
|
||||
def modify_service(self, name: str, **kwargs: object) -> "DockerComposeManager":
|
||||
"""Modify an existing service. Raises KeyError if not found.
|
||||
|
||||
Args:
|
||||
name (str): Name of the service to modify.
|
||||
**kwargs: Key-value pairs to update in the service configuration.
|
||||
|
||||
Raises:
|
||||
KeyError: If the service with the given name does not exist.
|
||||
|
||||
Returns:
|
||||
DockerComposeManager: self (for chaining)
|
||||
|
||||
"""
|
||||
services: dict[str, dict[str, Any]] = self._data["services"]
|
||||
if name not in services:
|
||||
msg: str = f"Service '{name}' not found."
|
||||
raise KeyError(msg)
|
||||
services[name].update(kwargs)
|
||||
self._dirty = True
|
||||
return self
|
||||
|
||||
def remove_service(self, name: str) -> "DockerComposeManager":
|
||||
"""Remove a service from the compose file.
|
||||
|
||||
Returns:
|
||||
DockerComposeManager: self (for chaining)
|
||||
|
||||
"""
|
||||
services: dict[str, dict[str, Any]] = self._data["services"]
|
||||
if name in services:
|
||||
del services[name]
|
||||
self._dirty = True
|
||||
return self
|
||||
|
||||
def save(self) -> None:
|
||||
"""Save the current state to the YAML file."""
|
||||
with Path(self.path).open("w", encoding="utf-8") as f:
|
||||
yaml.dump(self._data, f, sort_keys=False, indent=2)
|
||||
self._dirty = False
|
||||
|
||||
def __enter__(self) -> "DockerComposeManager":
|
||||
"""Enter the context manager and return self.
|
||||
|
||||
Returns:
|
||||
DockerComposeManager: The instance itself for context management.
|
||||
|
||||
"""
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
"""Exit the context manager and save the file if changes were made.
|
||||
|
||||
Args:
|
||||
exc_type: The exception type if an exception was raised, None otherwise.
|
||||
exc_val: The exception value if an exception was raised, None otherwise.
|
||||
exc_tb: The traceback if an exception was raised, None otherwise.
|
||||
|
||||
"""
|
||||
if self._dirty:
|
||||
self.save()
|
0
src/compose/py.typed
Normal file
0
src/compose/py.typed
Normal file
62
tests/test_compose_manager.py
Normal file
62
tests/test_compose_manager.py
Normal file
@ -0,0 +1,62 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from compose import DockerComposeManager
|
||||
|
||||
|
||||
def test_create_and_save_service(tmp_path: Path) -> None:
|
||||
compose_file: Path = tmp_path / "docker-compose.yml"
|
||||
manager = DockerComposeManager(str(compose_file))
|
||||
manager.create_service(
|
||||
name="web",
|
||||
image="nginx:latest",
|
||||
ports=["80:80"],
|
||||
environment={"ENV_VAR": "value"},
|
||||
).save()
|
||||
# Check file exists and content
|
||||
assert compose_file.exists()
|
||||
with compose_file.open() as f:
|
||||
data = yaml.safe_load(f)
|
||||
assert "web" in data["services"]
|
||||
assert data["services"]["web"]["image"] == "nginx:latest"
|
||||
assert data["services"]["web"]["ports"] == ["80:80"]
|
||||
assert data["services"]["web"]["environment"] == {"ENV_VAR": "value"}
|
||||
|
||||
|
||||
def test_modify_service(tmp_path: Path) -> None:
|
||||
compose_file: Path = tmp_path / "docker-compose.yml"
|
||||
manager = DockerComposeManager(str(compose_file))
|
||||
manager.create_service(name="web", image="nginx:latest").save()
|
||||
manager.modify_service(name="web", image="nginx:1.19", ports=["8080:80"]).save()
|
||||
with compose_file.open() as f:
|
||||
data = yaml.safe_load(f)
|
||||
assert data["services"]["web"]["image"] == "nginx:1.19"
|
||||
assert data["services"]["web"]["ports"] == ["8080:80"]
|
||||
|
||||
|
||||
def test_remove_service(tmp_path: Path) -> None:
|
||||
compose_file: Path = tmp_path / "docker-compose.yml"
|
||||
manager = DockerComposeManager(str(compose_file))
|
||||
manager.create_service(name="web", image="nginx:latest").save()
|
||||
manager.remove_service("web").save()
|
||||
with compose_file.open() as f:
|
||||
data = yaml.safe_load(f)
|
||||
assert "web" not in data["services"]
|
||||
|
||||
|
||||
def test_context_manager(tmp_path: Path) -> None:
|
||||
compose_file: Path = tmp_path / "docker-compose.yml"
|
||||
with DockerComposeManager(str(compose_file)) as manager:
|
||||
manager.create_service(name="web", image="nginx:latest")
|
||||
with compose_file.open() as f:
|
||||
data = yaml.safe_load(f)
|
||||
assert "web" in data["services"]
|
||||
|
||||
|
||||
def test_modify_nonexistent_service(tmp_path: Path) -> None:
|
||||
compose_file: Path = tmp_path / "docker-compose.yml"
|
||||
manager = DockerComposeManager(str(compose_file))
|
||||
with pytest.raises(KeyError):
|
||||
manager.modify_service("notfound", image="nginx:latest")
|
Reference in New Issue
Block a user