python

exercises

exercises.py🐍
"""
Docker and Deployment - Exercises

Practice containerization and deployment concepts.
Run with: pytest 28_docker_deployment/exercises.py -v
"""

from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Callable
import os
import json


# =============================================================================
# EXERCISE 1: Configuration Manager
# =============================================================================

class ConfigManager:
    """
    Implement a configuration manager for deployment.
    
    Requirements:
    - load_from_env(prefix) - load config from environment variables
    - load_from_file(path) - load from JSON file
    - load_from_dict(data) - load from dictionary
    - get(key, default) - get config value with default
    - require(key) - get required value, raise if missing
    - validate(schema) - validate against schema dict
    - to_dict() - export configuration
    
    Priority: env vars > file > dict > defaults
    
    Example:
        config = ConfigManager()
        config.load_from_dict({"port": 8000})
        config.load_from_env("APP_")  # APP_PORT=9000 in env
        
        assert config.get("port") == "9000"  # env takes priority
    """
    
    def __init__(self):
        # YOUR CODE HERE
        pass
    
    def load_from_env(self, prefix: str = "") -> 'ConfigManager':
        # YOUR CODE HERE
        pass
    
    def load_from_file(self, path: str) -> 'ConfigManager':
        # YOUR CODE HERE
        pass
    
    def load_from_dict(self, data: dict) -> 'ConfigManager':
        # YOUR CODE HERE
        pass
    
    def get(self, key: str, default: Any = None) -> Any:
        # YOUR CODE HERE
        pass
    
    def require(self, key: str) -> Any:
        """Get required value, raise KeyError if missing."""
        # YOUR CODE HERE
        pass
    
    def validate(self, schema: dict) -> list[str]:
        """
        Validate config against schema.
        Schema format: {"key": {"type": str, "required": True}}
        Returns list of validation errors.
        """
        # YOUR CODE HERE
        pass
    
    def to_dict(self) -> dict:
        # YOUR CODE HERE
        pass


def test_config_manager(tmp_path):
    """Test the ConfigManager."""
    # Create config file
    config_file = tmp_path / "config.json"
    config_file.write_text('{"host": "localhost", "port": 3000}')
    
    # Set environment variable
    os.environ["TEST_PORT"] = "8000"
    os.environ["TEST_DEBUG"] = "true"
    
    try:
        config = ConfigManager()
        config.load_from_dict({"port": 5000, "name": "myapp"})
        config.load_from_file(str(config_file))
        config.load_from_env("TEST_")
        
        # Environment takes priority
        assert config.get("port") == "8000"
        assert config.get("host") == "localhost"
        assert config.get("name") == "myapp"
        assert config.get("missing", "default") == "default"
        
        # Require
        assert config.require("debug") == "true"
        try:
            config.require("nonexistent")
            assert False, "Should raise KeyError"
        except KeyError:
            pass
            
    finally:
        del os.environ["TEST_PORT"]
        del os.environ["TEST_DEBUG"]


# =============================================================================
# EXERCISE 2: Health Check System
# =============================================================================

@dataclass
class CheckResult:
    """Result of a health check."""
    name: str
    healthy: bool
    message: str = ""
    latency_ms: float = 0
    metadata: dict = field(default_factory=dict)


class HealthCheckSystem:
    """
    Implement a comprehensive health check system.
    
    Requirements:
    - register(name, check_func, critical=True) - register a check
    - run_all() - run all checks, return overall result
    - run_one(name) - run single check
    - get_status() - return overall status ("healthy", "degraded", "unhealthy")
    
    A check function returns True (healthy) or raises an exception.
    "degraded" status when non-critical checks fail.
    "unhealthy" status when any critical check fails.
    
    Example:
        health = HealthCheckSystem()
        
        health.register("database", check_db, critical=True)
        health.register("cache", check_cache, critical=False)
        
        result = health.run_all()
        print(result["status"])  # "healthy", "degraded", or "unhealthy"
    """
    
    def __init__(self):
        # YOUR CODE HERE
        pass
    
    def register(self, name: str, check_func: Callable[[], bool], critical: bool = True):
        # YOUR CODE HERE
        pass
    
    def run_one(self, name: str) -> CheckResult:
        # YOUR CODE HERE
        pass
    
    def run_all(self) -> dict:
        """
        Run all checks and return result dict:
        {
            "status": "healthy" | "degraded" | "unhealthy",
            "timestamp": "...",
            "checks": [CheckResult, ...]
        }
        """
        # YOUR CODE HERE
        pass
    
    def get_status(self) -> str:
        # YOUR CODE HERE
        pass


def test_health_check_system():
    """Test the HealthCheckSystem."""
    health = HealthCheckSystem()
    
    # Healthy checks
    health.register("db", lambda: True, critical=True)
    health.register("cache", lambda: True, critical=False)
    
    result = health.run_all()
    assert result["status"] == "healthy"
    assert len(result["checks"]) == 2
    
    # Critical failure
    health = HealthCheckSystem()
    health.register("db", lambda: (_ for _ in ()).throw(Exception("DB down")), critical=True)
    health.register("cache", lambda: True, critical=False)
    
    result = health.run_all()
    assert result["status"] == "unhealthy"
    
    # Non-critical failure (degraded)
    health = HealthCheckSystem()
    health.register("db", lambda: True, critical=True)
    health.register("cache", lambda: (_ for _ in ()).throw(Exception("Cache down")), critical=False)
    
    result = health.run_all()
    assert result["status"] == "degraded"


# =============================================================================
# EXERCISE 3: Environment Variable Parser
# =============================================================================

class EnvParser:
    """
    Implement a typed environment variable parser.
    
    Requirements:
    - string(name, default) - get string value
    - integer(name, default) - get integer value
    - float(name, default) - get float value
    - boolean(name, default) - get boolean value
    - list(name, separator, default) - get list value
    - json(name, default) - get JSON value
    
    Boolean parsing: "true", "1", "yes" = True, others = False
    
    Example:
        env = EnvParser()
        port = env.integer("PORT", default=8000)
        debug = env.boolean("DEBUG", default=False)
        hosts = env.list("ALLOWED_HOSTS", separator=",", default=[])
    """
    
    def __init__(self, environ: dict = None):
        # YOUR CODE HERE
        pass
    
    def string(self, name: str, default: str = "") -> str:
        # YOUR CODE HERE
        pass
    
    def integer(self, name: str, default: int = 0) -> int:
        # YOUR CODE HERE
        pass
    
    def float_(self, name: str, default: float = 0.0) -> float:
        # YOUR CODE HERE
        pass
    
    def boolean(self, name: str, default: bool = False) -> bool:
        # YOUR CODE HERE
        pass
    
    def list_(self, name: str, separator: str = ",", default: list = None) -> list:
        # YOUR CODE HERE
        pass
    
    def json_(self, name: str, default: Any = None) -> Any:
        # YOUR CODE HERE
        pass


def test_env_parser():
    """Test the EnvParser."""
    environ = {
        "PORT": "8080",
        "DEBUG": "true",
        "RATIO": "0.75",
        "HOSTS": "localhost,example.com,api.example.com",
        "CONFIG": '{"key": "value"}',
        "ENABLED": "yes",
        "DISABLED": "no",
    }
    
    env = EnvParser(environ)
    
    assert env.string("PORT") == "8080"
    assert env.string("MISSING", "default") == "default"
    
    assert env.integer("PORT") == 8080
    assert env.integer("MISSING", 3000) == 3000
    
    assert env.float_("RATIO") == 0.75
    
    assert env.boolean("DEBUG") == True
    assert env.boolean("ENABLED") == True
    assert env.boolean("DISABLED") == False
    assert env.boolean("MISSING", True) == True
    
    hosts = env.list_("HOSTS")
    assert len(hosts) == 3
    assert "localhost" in hosts
    
    config = env.json_("CONFIG")
    assert config["key"] == "value"


# =============================================================================
# EXERCISE 4: Deployment Validator
# =============================================================================

@dataclass
class ValidationResult:
    """Deployment validation result."""
    valid: bool
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)


class DeploymentValidator:
    """
    Implement a pre-deployment validator.
    
    Requirements:
    - add_check(name, check_func, is_error=True) - add validation check
    - validate() - run all checks, return ValidationResult
    
    Check functions return (passed: bool, message: str)
    is_error=True means failure is an error, False means warning
    
    Example:
        validator = DeploymentValidator()
        
        validator.add_check("env_vars", check_required_env_vars)
        validator.add_check("disk_space", check_disk_space, is_error=False)
        
        result = validator.validate()
        if not result.valid:
            print("Cannot deploy:", result.errors)
    """
    
    def __init__(self):
        # YOUR CODE HERE
        pass
    
    def add_check(self, name: str, check_func: Callable[[], tuple[bool, str]], is_error: bool = True):
        # YOUR CODE HERE
        pass
    
    def validate(self) -> ValidationResult:
        # YOUR CODE HERE
        pass


def test_deployment_validator():
    """Test the DeploymentValidator."""
    validator = DeploymentValidator()
    
    # All pass
    validator.add_check("check1", lambda: (True, "OK"))
    validator.add_check("check2", lambda: (True, "OK"))
    
    result = validator.validate()
    assert result.valid == True
    assert len(result.errors) == 0
    
    # Error check fails
    validator = DeploymentValidator()
    validator.add_check("critical", lambda: (False, "Missing SECRET_KEY"))
    validator.add_check("optional", lambda: (True, "OK"), is_error=False)
    
    result = validator.validate()
    assert result.valid == False
    assert "SECRET_KEY" in result.errors[0]
    
    # Only warning fails
    validator = DeploymentValidator()
    validator.add_check("critical", lambda: (True, "OK"))
    validator.add_check("disk", lambda: (False, "Low disk space"), is_error=False)
    
    result = validator.validate()
    assert result.valid == True
    assert len(result.warnings) == 1


# =============================================================================
# EXERCISE 5: Secret Manager
# =============================================================================

class SecretManager:
    """
    Implement a simple secret manager for deployments.
    
    Requirements:
    - set(key, value) - store a secret
    - get(key) - retrieve a secret
    - delete(key) - remove a secret
    - exists(key) - check if secret exists
    - list_keys() - list all secret keys (not values!)
    - mask(value) - mask a secret value for logging
    - to_env_file() - export as .env file format
    
    Secrets should be stored encrypted (use simple XOR for demo).
    
    Example:
        secrets = SecretManager(encryption_key="mykey")
        secrets.set("DATABASE_PASSWORD", "secret123")
        
        password = secrets.get("DATABASE_PASSWORD")
        masked = secrets.mask(password)  # "sec*****23"
    """
    
    def __init__(self, encryption_key: str = "default-key"):
        # YOUR CODE HERE
        pass
    
    def _encrypt(self, value: str) -> str:
        """Simple XOR encryption for demo."""
        # YOUR CODE HERE
        pass
    
    def _decrypt(self, value: str) -> str:
        """Simple XOR decryption for demo."""
        # YOUR CODE HERE
        pass
    
    def set(self, key: str, value: str):
        # YOUR CODE HERE
        pass
    
    def get(self, key: str) -> str | None:
        # YOUR CODE HERE
        pass
    
    def delete(self, key: str) -> bool:
        # YOUR CODE HERE
        pass
    
    def exists(self, key: str) -> bool:
        # YOUR CODE HERE
        pass
    
    def list_keys(self) -> list[str]:
        # YOUR CODE HERE
        pass
    
    def mask(self, value: str, visible_chars: int = 3) -> str:
        """Mask secret, showing only first and last visible_chars."""
        # YOUR CODE HERE
        pass
    
    def to_env_file(self) -> str:
        """Export secrets as .env file format."""
        # YOUR CODE HERE
        pass


def test_secret_manager():
    """Test the SecretManager."""
    secrets = SecretManager("test-key")
    
    # Set and get
    secrets.set("DB_PASSWORD", "super-secret-123")
    assert secrets.get("DB_PASSWORD") == "super-secret-123"
    
    # Exists
    assert secrets.exists("DB_PASSWORD") == True
    assert secrets.exists("NONEXISTENT") == False
    
    # List keys
    secrets.set("API_KEY", "key123")
    keys = secrets.list_keys()
    assert "DB_PASSWORD" in keys
    assert "API_KEY" in keys
    
    # Mask
    masked = secrets.mask("super-secret-123", visible_chars=3)
    assert masked.startswith("sup")
    assert "*" in masked
    assert masked.endswith("123")
    
    # Delete
    assert secrets.delete("DB_PASSWORD") == True
    assert secrets.exists("DB_PASSWORD") == False
    
    # Env file
    env_content = secrets.to_env_file()
    assert "API_KEY=" in env_content


# =============================================================================
# EXERCISE 6: Dockerfile Generator
# =============================================================================

class DockerfileGenerator:
    """
    Implement a Dockerfile generator.
    
    Requirements:
    - base_image(image) - set base image
    - workdir(path) - set working directory
    - copy(src, dest) - add COPY instruction
    - run(command) - add RUN instruction
    - env(key, value) - add ENV instruction
    - expose(port) - add EXPOSE instruction
    - cmd(command) - set CMD instruction
    - entrypoint(command) - set ENTRYPOINT instruction
    - label(key, value) - add LABEL
    - generate() - generate Dockerfile content
    
    Example:
        df = DockerfileGenerator()
        content = (df
            .base_image("python:3.12-slim")
            .workdir("/app")
            .copy("requirements.txt", ".")
            .run("pip install -r requirements.txt")
            .copy(".", ".")
            .expose(8000)
            .cmd("python app.py")
            .generate())
    """
    
    def __init__(self):
        # YOUR CODE HERE
        pass
    
    def base_image(self, image: str) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def workdir(self, path: str) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def copy(self, src: str, dest: str) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def run(self, command: str) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def env(self, key: str, value: str) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def expose(self, port: int) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def cmd(self, command: str | list) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def entrypoint(self, command: str | list) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def label(self, key: str, value: str) -> 'DockerfileGenerator':
        # YOUR CODE HERE
        pass
    
    def generate(self) -> str:
        # YOUR CODE HERE
        pass


def test_dockerfile_generator():
    """Test the DockerfileGenerator."""
    df = DockerfileGenerator()
    content = (df
        .base_image("python:3.12-slim")
        .label("maintainer", "dev@example.com")
        .workdir("/app")
        .copy("requirements.txt", ".")
        .run("pip install --no-cache-dir -r requirements.txt")
        .copy(".", ".")
        .env("PYTHONUNBUFFERED", "1")
        .expose(8000)
        .cmd("python app.py")
        .generate())
    
    assert "FROM python:3.12-slim" in content
    assert "WORKDIR /app" in content
    assert "COPY requirements.txt ." in content
    assert "RUN pip install" in content
    assert "EXPOSE 8000" in content
    assert "CMD" in content


# =============================================================================
# EXERCISE 7: Rollback Manager
# =============================================================================

@dataclass
class Deployment:
    """A deployment record."""
    version: str
    timestamp: datetime
    status: str  # "success", "failed", "rolled_back"
    metadata: dict = field(default_factory=dict)


class RollbackManager:
    """
    Implement a deployment rollback manager.
    
    Requirements:
    - record(version, status, metadata) - record a deployment
    - get_current() - get current active deployment
    - get_previous() - get previous successful deployment
    - get_history(limit) - get deployment history
    - rollback() - mark current as rolled back, return previous version
    - can_rollback() - check if rollback is possible
    
    Example:
        manager = RollbackManager()
        
        manager.record("v1.0.0", "success")
        manager.record("v1.1.0", "success")
        manager.record("v1.2.0", "failed")
        
        if manager.can_rollback():
            prev = manager.rollback()  # Returns "v1.1.0"
    """
    
    def __init__(self):
        # YOUR CODE HERE
        pass
    
    def record(self, version: str, status: str, metadata: dict = None) -> Deployment:
        # YOUR CODE HERE
        pass
    
    def get_current(self) -> Deployment | None:
        # YOUR CODE HERE
        pass
    
    def get_previous(self) -> Deployment | None:
        """Get previous successful deployment."""
        # YOUR CODE HERE
        pass
    
    def get_history(self, limit: int = 10) -> list[Deployment]:
        # YOUR CODE HERE
        pass
    
    def can_rollback(self) -> bool:
        # YOUR CODE HERE
        pass
    
    def rollback(self) -> str | None:
        """Rollback to previous version, return that version."""
        # YOUR CODE HERE
        pass


def test_rollback_manager():
    """Test the RollbackManager."""
    manager = RollbackManager()
    
    # Record deployments
    manager.record("v1.0.0", "success")
    manager.record("v1.1.0", "success")
    manager.record("v1.2.0", "failed")
    
    # Current is the failed one
    current = manager.get_current()
    assert current.version == "v1.2.0"
    assert current.status == "failed"
    
    # Previous is last successful
    previous = manager.get_previous()
    assert previous.version == "v1.1.0"
    
    # Can rollback
    assert manager.can_rollback() == True
    
    # Do rollback
    rollback_version = manager.rollback()
    assert rollback_version == "v1.1.0"
    
    # Current should now be marked as rolled back
    history = manager.get_history()
    assert any(d.status == "rolled_back" for d in history)


# =============================================================================
# RUN TESTS
# =============================================================================

if __name__ == "__main__":
    import pytest
    pytest.main([__file__, "-v"])
Exercises - Python Tutorial | DeepML