Skip to content

Conversation

@Pavan19047
Copy link

Pull Request: Droplet Actions Helpers

📋 Description

This PR introduces a new helpers module to the pydo package that provides intuitive, well-documented wrapper functions for common Droplet Actions operations. These helpers abstract away the complexity of manually constructing request bodies and navigating nested response structures, making it significantly easier for users to perform common droplet operations.

🎯 Motivation & Context

Problem

Currently, users interacting with droplet actions need to:

  1. Understand the internal API structure - Know the exact request body format for each action type
  2. Manually construct request bodies - Build dictionaries with correct keys and values
  3. Navigate nested responses - Extract the action object from response wrappers
  4. Handle errors generically - No contextual error messages for debugging

Example of current approach:

from pydo import Client

client = Client(token="your_token")

# Users must know to construct this exact body structure
response = client.droplet_actions.post(
    droplet_id=123456,
    body={"type": "power_on"}
)
action = response["action"]  # Must remember to unwrap

# Complex actions require knowing all parameters
response = client.droplet_actions.post(
    droplet_id=123456,
    body={
        "type": "resize",
        "size": "s-2vcpu-4gb",
        "disk": True
    }
)
action = response["action"]

Solution

This PR provides convenience helpers that:

  • Abstract request body construction - Users don't need to know internal structure
  • Normalize responses - Return the action dict directly
  • Provide clear interfaces - Named parameters instead of dictionary keys
  • Add contextual error handling - Include droplet ID and action type in errors
  • Include comprehensive documentation - Docstrings with examples for every function

Example with helpers:

from pydo import Client, power_on, resize, snapshot

client = Client(token="your_token")

# Simple, intuitive, and self-documenting
action = power_on(client, droplet_id=123456)
print(f"Status: {action['status']}")  # Direct access

# Clear parameter names, no guessing
action = resize(client, droplet_id=123456, size="s-2vcpu-4gb", disk=True)

# Optional parameters are obvious
action = snapshot(client, droplet_id=123456, name="my-backup")

🆕 What's New

Helper Functions (11 Total)

All helpers are exposed at the package root for convenient importing:

from pydo import (
    perform_action,    # Generic function for any action
    power_on,          # Power on a droplet
    power_off,         # Hard shutdown
    reboot,            # Graceful reboot
    shutdown,          # Graceful shutdown
    power_cycle,       # Power cycle (reset button)
    snapshot,          # Take a snapshot
    resize,            # Resize a droplet
    rename,            # Rename a droplet
    rebuild,           # Rebuild from an image
    password_reset,    # Reset root password
)

1. perform_action(client, droplet_id, action_type, **extra)

Generic action executor - Handles any droplet action type

# Use for any action, including future ones not explicitly wrapped
action = perform_action(client, 123456, "enable_backups")
action = perform_action(client, 123456, "resize", size="s-2vcpu-4gb", disk=True)

Features:

  • Constructs request body automatically
  • Normalizes responses (unwraps action from response)
  • Wraps all errors with contextual information
  • Extensible for any action type

2-11. Convenience Wrappers

Function Purpose Parameters
power_on(client, droplet_id) Power on a droplet None
power_off(client, droplet_id) Hard shutdown None
reboot(client, droplet_id) Graceful reboot None
shutdown(client, droplet_id) Graceful shutdown None
power_cycle(client, droplet_id) Power cycle (reset) None
snapshot(client, droplet_id, name=None) Take a snapshot name: Optional snapshot name
resize(client, droplet_id, size, disk=False) Resize droplet size: Size slug (required)
disk: Permanent disk resize
rename(client, droplet_id, name) Rename droplet name: New droplet name
rebuild(client, droplet_id, image) Rebuild from image image: Image ID or slug
password_reset(client, droplet_id) Reset root password None

Key Features

🎁 Response Normalization

Responses are automatically unwrapped to return the action dict directly:

# Before: Nested response
response = client.droplet_actions.post(droplet_id=123, body={"type": "reboot"})
action = response["action"]  # Must manually unwrap

# After: Direct action access
action = reboot(client, droplet_id=123)  # Already unwrapped

🛡️ Enhanced Error Handling

All exceptions are wrapped with contextual information:

try:
    action = power_on(client, droplet_id=999999)
except HttpResponseError as e:
    # Error message includes action type and droplet ID
    print(e)  # "Failed to perform 'power_on' action on droplet 999999: Droplet not found"

📦 Package Root Exports

Import directly from pydo without navigating submodules:

# ✅ Clean imports
from pydo import power_on, resize, snapshot

# ❌ No need for this anymore
from pydo.helpers.droplet_actions import power_on, resize, snapshot

📝 Comprehensive Documentation

Every function includes:

  • Detailed docstrings
  • Parameter descriptions
  • Return value documentation
  • Usage examples
  • Type hints for IDE support

🔧 Type Safety

Full type annotations for better IDE autocompletion and type checking:

def resize(
    client, 
    droplet_id: int, 
    size: str, 
    disk: bool = False
) -> Dict[str, Any]:
    ...

📁 Files Changed

New Files (3)

  1. src/pydo/helpers/__init__.py (29 lines)

    • Helper module initialization
    • Exports all droplet action functions
  2. src/pydo/helpers/droplet_actions.py (286 lines)

    • Core implementation of all helper functions
    • Generic perform_action function
    • 10 convenience wrapper functions
    • Full documentation and type hints
  3. tests/mocked/test_helpers_droplet_actions.py (323 lines)

    • 19 comprehensive test cases
    • FakeClient and FakeDropletActions mocks
    • Tests all functions and error scenarios

Modified Files (1)

  1. src/pydo/__init__.py
    • Added imports for all helper functions
    • Exposed helpers in __all__ for package root access

Total Changes: 4 files changed, 664 insertions(+)

✅ Testing

Unit Test Coverage

19 test cases covering all functionality:

✅ test_perform_action_success                    - Basic action execution
✅ test_perform_action_with_extra_params          - Actions with parameters
✅ test_perform_action_without_action_key         - Response normalization
✅ test_perform_action_http_error                 - HTTP error wrapping
✅ test_perform_action_unexpected_error           - Generic error wrapping
✅ test_power_on                                  - Power on helper
✅ test_power_off                                 - Power off helper
✅ test_reboot                                    - Reboot helper
✅ test_shutdown                                  - Shutdown helper
✅ test_power_cycle                               - Power cycle helper
✅ test_snapshot_without_name                     - Snapshot without name
✅ test_snapshot_with_name                        - Snapshot with name
✅ test_resize_without_disk                       - Resize without disk
✅ test_resize_with_disk                          - Resize with disk
✅ test_rename                                    - Rename helper
✅ test_rebuild_with_slug                         - Rebuild with image slug
✅ test_rebuild_with_image_id                     - Rebuild with image ID
✅ test_password_reset                            - Password reset helper
✅ test_all_helpers_handle_errors                 - Error propagation

Test Results

================================= test session starts ==================================
collected 19 items

tests/mocked/test_helpers_droplet_actions.py::test_perform_action_success PASSED [  5%]
tests/mocked/test_helpers_droplet_actions.py::test_perform_action_with_extra_params PASSED [ 10%]
tests/mocked/test_helpers_droplet_actions.py::test_perform_action_without_action_key PASSED [ 15%]
tests/mocked/test_helpers_droplet_actions.py::test_perform_action_http_error PASSED [ 21%]
tests/mocked/test_helpers_droplet_actions.py::test_perform_action_unexpected_error PASSED [ 26%]
tests/mocked/test_helpers_droplet_actions.py::test_power_on PASSED                [ 31%]
tests/mocked/test_helpers_droplet_actions.py::test_power_off PASSED               [ 36%]
tests/mocked/test_helpers_droplet_actions.py::test_reboot PASSED                  [ 42%]
tests/mocked/test_helpers_droplet_actions.py::test_shutdown PASSED                [ 47%]
tests/mocked/test_helpers_droplet_actions.py::test_power_cycle PASSED             [ 52%]
tests/mocked/test_helpers_droplet_actions.py::test_snapshot_without_name PASSED   [ 57%]
tests/mocked/test_helpers_droplet_actions.py::test_snapshot_with_name PASSED      [ 63%]
tests/mocked/test_helpers_droplet_actions.py::test_resize_without_disk PASSED     [ 68%]
tests/mocked/test_helpers_droplet_actions.py::test_resize_with_disk PASSED        [ 73%]
tests/mocked/test_helpers_droplet_actions.py::test_rename PASSED                  [ 78%]
tests/mocked/test_helpers_droplet_actions.py::test_rebuild_with_slug PASSED       [ 84%]
tests/mocked/test_helpers_droplet_actions.py::test_rebuild_with_image_id PASSED   [ 89%]
tests/mocked/test_helpers_droplet_actions.py::test_password_reset PASSED          [ 94%]
tests/mocked/test_helpers_droplet_actions.py::test_all_helpers_handle_errors PASSED [100%]

================================== 19 passed in 0.13s ==================================

No Credentials Required

All tests use mocked clients - no DIGITALOCEAN_TOKEN needed for local testing:

class FakeClient:
    """Fake client for testing."""
    def __init__(self):
        self.droplet_actions = FakeDropletActions()

class FakeDropletActions:
    """Fake DropletActions operations for testing."""
    def __init__(self):
        self.post = Mock()

Running Tests Locally

# Run the new helper tests
pytest tests/mocked/test_helpers_droplet_actions.py -v

# Run all mocked tests (no token required)
pytest -m "not integration" -v

# Run with coverage
pytest tests/mocked/test_helpers_droplet_actions.py --cov=src/pydo/helpers

🎨 Code Quality

Formatting & Linting ✅

  • Black: All files formatted (black --check passes)
  • Flake8: Zero linting errors (max-line-length=88)
  • Type Hints: Complete type annotations on all functions
  • Docstrings: Google-style docstrings with examples

Code Style

# Formatting check
$ python -m black src/pydo/helpers/ tests/mocked/test_helpers_droplet_actions.py
All done! ✨ 🍰 ✨
4 files left unchanged.

# Linting check
$ python -m flake8 src/pydo/helpers/ tests/mocked/test_helpers_droplet_actions.py --max-line-length=88
# No output = no errors ✅

📚 Usage Examples

Basic Operations

from pydo import Client, power_on, power_off, reboot

client = Client(token="your_digital_ocean_token")

# Power on a droplet
action = power_on(client, droplet_id=123456)
print(f"Action ID: {action['id']}, Status: {action['status']}")

# Graceful reboot
action = reboot(client, droplet_id=123456)

# Hard shutdown
action = power_off(client, droplet_id=123456)

Operations with Parameters

from pydo import Client, snapshot, resize, rename, rebuild

client = Client(token="your_digital_ocean_token")

# Take a named snapshot
action = snapshot(client, droplet_id=123456, name="pre-upgrade-backup")

# Resize with permanent disk expansion
action = resize(
    client, 
    droplet_id=123456, 
    size="s-2vcpu-4gb", 
    disk=True
)

# Rename a droplet
action = rename(client, droplet_id=123456, name="production-web-01")

# Rebuild from Ubuntu image
action = rebuild(client, droplet_id=123456, image="ubuntu-22-04-x64")

Error Handling

from pydo import Client, power_on
from azure.core.exceptions import HttpResponseError

client = Client(token="your_digital_ocean_token")

try:
    action = power_on(client, droplet_id=999999)
except HttpResponseError as e:
    print(f"Error: {e}")
    # Output: "Failed to perform 'power_on' action on droplet 999999: Droplet not found"

Using the Generic Function

from pydo import Client, perform_action

client = Client(token="your_digital_ocean_token")

# Use for any action type, including future ones
action = perform_action(client, 123456, "enable_backups")
action = perform_action(client, 123456, "disable_backups")
action = perform_action(client, 123456, "enable_ipv6")

# Actions with parameters
action = perform_action(
    client, 
    123456, 
    "change_backup_policy",
    plan="weekly",
    weekday="sunday"
)

🔄 Backward Compatibility

✅ No Breaking Changes

This PR is purely additive. All existing functionality remains unchanged:

  • ✅ Existing client.droplet_actions.post() still works
  • ✅ No changes to existing API interfaces
  • ✅ No modifications to existing tests
  • ✅ No changes to existing exports (only additions)

Migration Path

Users can adopt the helpers gradually:

# Old code continues to work
response = client.droplet_actions.post(droplet_id=123, body={"type": "reboot"})

# New code can be introduced incrementally
from pydo import reboot
action = reboot(client, droplet_id=123)

🚀 Benefits

For End Users

  1. Easier to use - Intuitive function names and parameters
  2. Less error-prone - No manual body construction
  3. Better discoverability - IDE autocomplete shows all available actions
  4. Clearer code - Self-documenting function calls
  5. Faster development - Less time looking up API documentation

For the Project

  1. Better developer experience - Makes pydo more competitive with other SDKs
  2. Reduced support burden - Fewer questions about basic operations
  3. Extensible pattern - Template for future helper modules (volumes, images, etc.)
  4. Well-tested - Comprehensive test coverage prevents regressions
  5. Documentation as code - Examples in docstrings stay up-to-date

📋 Checklist

  • Code follows project style guidelines (black + flake8)
  • Added comprehensive unit tests (19 test cases)
  • All tests pass locally (19/19 passed)
  • Documentation/docstrings added (all functions documented)
  • No breaking changes (fully backward compatible)
  • Type hints included (full type annotations)
  • Helper functions exposed at package root
  • Error handling implemented (all exceptions wrapped)
  • Code formatted with black
  • Passes flake8 linting
  • No credentials required for tests (uses mocks)

🔮 Future Enhancements

This PR establishes a pattern that can be extended to other resources:

  • pydo.helpers.volume_actions - Helpers for volume operations
  • pydo.helpers.image_actions - Helpers for image operations
  • pydo.helpers.kubernetes_actions - Helpers for Kubernetes cluster operations
  • pydo.helpers.load_balancer_helpers - Helpers for load balancer management

📝 Additional Notes

Design Decisions

  1. Why separate helpers module?

    • Keeps generated code clean (no manual edits to auto-generated files)
    • Makes it easy to add more helpers without touching core code
    • Clear separation between low-level API and high-level helpers
  2. Why normalize responses?

    • Most users only care about the action object, not the wrapper
    • Reduces cognitive load (fewer steps to get useful data)
    • Consistent with user expectations from other SDKs
  3. Why expose at package root?

    • Improves discoverability (shows up in dir(pydo))
    • Matches common Python SDK patterns
    • Reduces import boilerplate

Integration with CI/CD

  • Mocked tests run without credentials (safe for PR checks)
  • Integration tests can use real API in separate CI jobs
  • No changes needed to existing CI/CD pipelines

🙏 Review Focus Areas

  1. API Design - Are function names and parameters intuitive?
  2. Error Handling - Is error wrapping helpful without being too verbose?
  3. Documentation - Are docstrings clear and examples useful?
  4. Test Coverage - Are there edge cases not covered?
  5. Backward Compatibility - Any potential breaking changes?

Ready for review! This PR adds significant value to the pydo SDK by making common operations more accessible and developer-friendly. 🚀

- Add generic perform_action function for droplet actions
- Add convenience wrappers: power_on, power_off, reboot, shutdown, power_cycle
- Add snapshot, resize, rename, rebuild, password_reset helpers
- Expose all helpers in package root for easy import
- Add comprehensive unit tests with FakeClient mocking
- All tests pass without requiring DIGITALOCEAN_TOKEN
- Code formatted with black and passes flake8 linting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant