A cross-platform Python deployment framework for automated remote server deployments via SSH
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75db1d78e7cc681c4054030d5f0b80991fcbcb9af03f24f26db0a093cc3b6e34
|
|
| MD5 |
ba9c27948f1cca4138cba074f9330874
|
|
| BLAKE2b-256 |
fc0dcc7b52ad81aa36c914a3b5edcf80682e6222c9cb2ffdbc6685f50de8d077
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d65846bd75113712e18fca9e9ac08eefdc2b0d0c00f5b403c347fed57139cd3
|
|
| MD5 |
433c0b629329d812b6138e63dc88a202
|
|
| BLAKE2b-256 |
f9ea9f3f56c9e6872c713c0ff3052b95066b20b8103677afcd7f154c1bac135f
|