Compare commits

...

2 Commits

Author SHA1 Message Date
63160d682f 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.
2025-06-18 03:45:48 +02:00
49e72e82a0 Update .gitignore 2025-06-18 03:45:23 +02:00
8 changed files with 256 additions and 5 deletions

7
.github/copilot-instructions.md vendored Normal file
View 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.

9
.gitignore vendored
View File

@ -86,27 +86,27 @@ ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
@ -173,4 +173,3 @@ cython_debug/
# PyPI configuration file
.pypirc

0
example/__init__.py Normal file
View File

36
example/basic_example.py Normal file
View 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
View 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
View 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
View File

View 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")