Skip to main content

A cross-platform Python deployment framework for automated remote server deployments via SSH

Project description

PyDaffodil

Cross-Platform Deployment Automation Framework for Python

PyPI version Python versions PyPI downloads Stars License


Overview

PyDaffodil is a lightweight, declarative deployment automation framework for Python that simplifies remote server deployments over SSH. It provides a clear, step-oriented API for local commands, remote execution, and archive-based file transfer, with optional watch-based triggers and multi-host deployments via Ansible-style inventory.ini files.

Key Features

  • Archive-Based File Transfer — Packages local paths, transfers efficiently, and extracts on the remote host
  • Cross-Platform Support — Runs on Windows, Linux, and macOS
  • SSH via Paramiko — Key-based authentication (RSA, ECDSA, Ed25519, and others supported by Paramiko)
  • Step-by-Step Execution — Chain deployment steps with readable progress output
  • Ignore Pattern Support.scpignore (or custom path) to exclude paths from transfers
  • Colored Terminal Output — Clear logs via Colorama
  • Progress Feedback — Transfer progress with tqdm
  • Watch-Based Workflows (watch()) — Trigger deploys on file changes and/or Git activity (commits, merges, tags)
  • Multi-Host Deployments — Same steps across multiple servers using inventory.ini groups

Documentation and Examples

For hands-on usage, see the example/ directory:

  • example/publish.py — Basic scripted deployment
  • example/publish-multi.py — Multi-host deployment with inventory.ini
  • example/publish-watch.py — File and Git–triggered deploys with watch()
  • example/.daffodil.yml — Reference schema for the YAML CLI

Installation

pip install pydaffodil

Requires a supported Python 3.x (see PyPI classifiers) and network access to the remote host over SSH.


Quick Start

from pydaffodil import Daffodil

deployer = Daffodil(
    remote_user="deployer",
    remote_host="231.142.34.222",
    remote_path="/var/www/myapp",
    port=22,  # optional; default 22
)

steps = [
    {
        "step": "Transfer application files",
        "command": lambda: deployer.transfer_files("./dist", "/var/www/myapp"),
    },
    {
        "step": "Install dependencies",
        "command": lambda: deployer.ssh_command(
            "cd /var/www/myapp && npm install --production=false"
        ),
    },
    {
        "step": "Restart application",
        "command": lambda: deployer.ssh_command("pm2 restart myapp"),
    },
]

deployer.deploy(steps)

API Reference

Constructor

Daffodil(
    remote_user=None,
    remote_host=None,
    remote_path=None,
    port=22,
    ssh_key_path=None,
    ssh_key_pass=None,
    scp_ignore=".scpignore",
    inventory=None,   # path to inventory.ini (multi-host mode)
    group=None,       # inventory group name (required if inventory is set)
)

In single-host mode, remote_user and remote_host are required. In inventory mode, hosts are loaded from inventory.ini and group must identify the section to use.

Methods

transfer_files(local_path, destination_path=None)

Transfers a local file or directory to the remote server using an archive step, then extracts on the remote side. Honors .scpignore patterns.

run_command(command)

Runs a shell command on the local machine.

ssh_command(command)

Runs a command on the remote server over the active SSH session.

make_directory(directory_name)

Creates a directory on the remote server (under the configured remote context).

deploy(steps)

Runs deployment steps in order. Each step is a dict with:

  • step — Human-readable label
  • command — Callable (typically a lambda) returning the operation result

In inventory mode, the same steps are executed sequentially per host.

watch(...)

Returns a watch session with a .deploy(steps) method. Configure file paths, Git repo path, branches, tags, events, debounce, and polling interval.

deployer.watch(
    paths=["./dist", "./src"],
    debounce=2000,          # ms between eligible deploys after a trigger
    repo_path=".",
    branches=["main", "staging"],
    tags=True,
    tag_pattern=r"^v\d+\.\d+\.\d+$",  # regex string; optional
    events=["commit", "merge", "tag"],
    interval=5000,          # poll interval in ms
).deploy(steps)

Advanced Topics

Archive-Based Transfer

PyDaffodil builds an archive of the selected local content, transfers it, and extracts it remotely. This reduces round-trips and works well for larger trees and slower links.

Ignore Patterns (.scpignore)

Place a .scpignore in your project (or point scp_ignore at another file). Patterns exclude matching paths from transfer, similar in spirit to .gitignore-style workflows.

SSH Keys

Provide ssh_key_path (and ssh_key_pass if the key is encrypted), or rely on Paramiko’s default key discovery where applicable.


Best Practices

SSH Access

Ensure key-based login works before automating:

ssh-keygen -t ed25519 -C "you@example.com"
ssh-copy-id deployer@your-server
ssh deployer@your-server

Error Handling

Wrap deploy scripts in try / except and exit with a non-zero status in CI.

Secrets

Prefer environment variables or a secrets manager for hosts, users, and keys—not hard-coded credentials in source control.

Conditional Steps

Build the steps list dynamically (e.g. only run migrations in production) using ordinary Python control flow.


Configuration Options

Option Type Default Description
remote_user str SSH username (single-host mode)
remote_host str Hostname or IP (single-host mode)
remote_path str auto / . Default remote base path
port int 22 SSH port
ssh_key_path str None Path to private key
ssh_key_pass str None Key passphrase, if needed
scp_ignore str ".scpignore" Ignore file path
inventory str None Path to inventory.ini (multi-host)
group str None Inventory group name (e.g. webservers)

Watch-Based CI/CD

Use watch() to run the same deploy(steps) pipeline when files change or Git state updates. See example/ for patterns; combine paths with repo_path for file + Git triggers.


Multi-Host Deployments with inventory.ini

Use an Ansible-style INI file to target a group of hosts with one script.

Example inventory.ini

[webservers]
server1 host=231.142.34.222 user=deployer port=22
server2 host=231.142.34.223 user=deployer
server3 host=231.142.34.224 user=ubuntu port=2200

Programmatic usage

from pydaffodil import Daffodil

deployer = Daffodil(
    inventory="./inventory.ini",
    group="webservers",
    remote_path="/var/www/myapp",
)

deployer.deploy(steps)

See example/publish-multi.py and example/inventory.ini for a complete layout.


Requirements

  • Python 3.x (see PyPI for supported versions)
  • SSH connectivity to the remote host
  • Dependencies (installed with the package): paramiko, tqdm, colorama

YAML CLI Deployment

PyDaffodil includes a CLI that reads .daffodil.yml:

pydaffodil --config example/.daffodil.yml
pydaffodil --config example/.daffodil.yml --watch

The config filename must be exactly .daffodil.yml.

Other official CLIs (same YAML): JSDaffodil uses jsdaffodil --config; GoDaffodil uses godaffodil run --config (no other subcommands).

Host resolution (CLI)

Hosts are resolved in this order:

  1. Inline hosts in the YAML file (if present and non-empty)
  2. inventoryFile + inventoryGroup (Ansible-style inventory.ini)
  3. Top-level remoteHost / remoteUser (or snake_case equivalents) for a single default host

Optional inventory reference:

inventoryFile: inventory.ini
inventoryGroup: webservers

Contributing

Contributions are welcome. Open an issue to discuss larger changes, then submit a pull request with a clear description and tests where appropriate.

The library code lives under src/pydaffodil/.

Local setup with uv

From the repository root:

uv sync

That creates .venv/, installs the project in editable mode, and pulls dev dependencies (build tools, and so on). Use uv run … so commands use that environment without activating the venv manually.

CLI in development

Run the CLI through uv so it uses the local package:

uv run pydaffodil

Other equivalent entry points:

uv run python -m pydaffodil
uv run python -m pydaffodil.cli

The CLI looks for a config file named .daffodil.yml in the current working directory. To try the sample config under example/:

uv run --directory example pydaffodil

Or:

cd example
uv run pydaffodil

(Add --watch for watch mode when your YAML defines it.)

Tests

From the repository root:

uv run python -m unittest discover -s tests -v

License

MIT License


Acknowledgments

Sister projects: JSDaffodil (Node.js), GoDaffodil (Go).


Made with care by Mark Wayne B. Menorca

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydaffodil-1.2.1.tar.gz (6.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydaffodil-1.2.1-py3-none-any.whl (6.5 kB view details)

Uploaded Python 3

File details

Details for the file pydaffodil-1.2.1.tar.gz.

File metadata

  • Download URL: pydaffodil-1.2.1.tar.gz
  • Upload date:
  • Size: 6.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pydaffodil-1.2.1.tar.gz
Algorithm Hash digest
SHA256 75db1d78e7cc681c4054030d5f0b80991fcbcb9af03f24f26db0a093cc3b6e34
MD5 ba9c27948f1cca4138cba074f9330874
BLAKE2b-256 fc0dcc7b52ad81aa36c914a3b5edcf80682e6222c9cb2ffdbc6685f50de8d077

See more details on using hashes here.

File details

Details for the file pydaffodil-1.2.1-py3-none-any.whl.

File metadata

  • Download URL: pydaffodil-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 6.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for pydaffodil-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9d65846bd75113712e18fca9e9ac08eefdc2b0d0c00f5b403c347fed57139cd3
MD5 433c0b629329d812b6138e63dc88a202
BLAKE2b-256 f9ea9f3f56c9e6872c713c0ff3052b95066b20b8103677afcd7f154c1bac135f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page