PyDaffodil
Cross-Platform Deployment Automation Framework for Python
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.inigroups
Documentation and Examples
For hands-on usage, see the example/ directory:
example/publish.py— Basic scripted deploymentexample/publish-multi.py— Multi-host deployment withinventory.iniexample/publish-watch.py— File and Git–triggered deploys withwatch()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 labelcommand— Callable (typically alambda) 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:
- Inline
hostsin the YAML file (if present and non-empty) inventoryFile+inventoryGroup(Ansible-styleinventory.ini)- 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
Acknowledgments
Sister projects: JSDaffodil (Node.js), GoDaffodil (Go).
Made with care by Mark Wayne B. Menorca
Metadata
Release files for pydaffodil 1.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pydaffodil-1.2.1.tar.gz | 6.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pydaffodil-1.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 12.8 kB
Release files / pydaffodil-1.2.1.tar.gz
| Download URL | pydaffodil-1.2.1.tar.gz |
|---|---|
| Size | 6.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
75db1d78e7cc681c4054030d5f0b80991fcbcb9af03f24f26db0a093cc3b6e34
|
|
BLAKE2b-256 checksum How to use checksums |
fc0dcc7b52ad81aa36c914a3b5edcf80682e6222c9cb2ffdbc6685f50de8d077
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / pydaffodil-1.2.1-py3-none-any.whl
| Download URL | pydaffodil-1.2.1-py3-none-any.whl |
|---|---|
| Size | 6.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9d65846bd75113712e18fca9e9ac08eefdc2b0d0c00f5b403c347fed57139cd3
|
|
BLAKE2b-256 checksum How to use checksums |
f9ea9f3f56c9e6872c713c0ff3052b95066b20b8103677afcd7f154c1bac135f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|