Metadata-Version: 2.4
Name: fastops
Version: 0.0.2
Summary: End-to-end developer operations toolkit: Docker, VPS, DNS, Caddy, and CI/CD from Python
Project-URL: Repository, https://github.com/Karthik777/fastops
Project-URL: Documentation, https://Karthik777.github.io/fastops/
Author-email: 71293 <karthik.rajgopal@hotmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: caddy,cloudflare,devops,docker,nbdev,vps
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.10
Requires-Dist: cloudflare>=4.3.1
Requires-Dist: fastcloudinit>=0.0.5
Requires-Dist: fastcore>=1.12.14
Requires-Dist: notebook
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# fastops


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Install

``` sh
pip install fastops
```

Requires Python 3.10+ and a `docker` (or `podman`) CLI on your PATH.

## The Dockerfile Builder

[`Dockerfile`](https://Karthik777.github.io/fastops/core.html#dockerfile)
is an immutable, fluent builder — every method returns a new instance,
so you can safely branch, compose, or pass it to functions.

``` python
from fastops import Dockerfile

df = (Dockerfile()
    .from_('python', '3.12-slim')
    .workdir('/app')
    .copy('requirements.txt', '.')
    .run('pip install --no-cache-dir -r requirements.txt')
    .copy('.', '.')
    .expose(8080)
    .cmd(['python', 'app.py']))

print(df)
```

### Batteries included: `apt_install` and a built-in escape hatch

`apt_install(*pkgs, y=False)` chains `apt-get update && apt-get install`
for you. Any Dockerfile keyword not explicitly modelled is available via
`__getattr__` — just call it as a method.

``` python
df_ubuntu = (Dockerfile()
    .from_('ubuntu', '22.04')
    .apt_install('curl', 'git', y=True)
    .run('pip install uv'))

# Any unknown attribute becomes an instruction: df.KEYWORD(args)
df_scratch = (Dockerfile()
    .from_('scratch')
    .add('binary', '/binary')
    .entrypoint(['/binary']))

print(df_ubuntu)
print()
print(df_scratch)
```

### Multi-stage builds

Chain multiple `from_()` calls — the builder handles stage aliases
naturally.

``` python
df_ms = (Dockerfile()
    .from_('golang:1.21', as_='builder')
    .workdir('/src')
    .copy('.', '.')
    .run('go build -o /app')
    .from_('alpine')
    .copy('/app', '/app', from_='builder')
    .cmd(['/app']))

print(df_ms)
```

`Dockerfile.load(path)` parses an existing file into the builder for
further chaining. `df.save(path)` writes it back and returns the `Path`.

## Building and Running Images

These helpers require a running Docker daemon. They wrap `docker build`,
`docker run`, `docker ps`, etc. via subprocess.

``` python
from fastops import Dockerfile, run, test, containers, images, stop, logs, rm, rmi

df = Dockerfile().from_('python', '3.12-slim').cmd(['python', '-c', 'print("hi")'])

img = df.build(tag='myapp:latest')        # saves Dockerfile + runs docker build
ok  = test(img, 'python -c "import os"')   # True if exit code 0
cid = run(img, detach=True, ports={8080: 8080}, name='myapp')

print(containers())    # ['myapp']
print(logs('myapp', n=5))

stop('myapp'); rm('myapp'); rmi(img)
```

### Raw CLI access via `dk`

For anything not covered by helpers, `dk` (a
[`Docker()`](https://Karthik777.github.io/fastops/core.html#docker)
singleton) dispatches any subcommand. kwargs become flags using the same
convention: single-char `k=v` → `-k v`, multi-char `key=v` → `--key=v`.

``` python
from fastops import dk

try:
    print(dk.version())
except Exception as e:
    print(f'Docker not running: {e}')

# Equivalent shell commands:
# dk.ps(format='{{.Names}}', a=True)   → docker ps --format={{.Names}} -a
# dk.image('prune', f=True)            → docker image prune -f
# dk.build('.', t='myapp', rm=True)    → docker build . -t myapp --rm
```

## Docker Compose

[`Compose`](https://Karthik777.github.io/fastops/compose.html#compose)
is a fluent builder for `docker-compose.yml`. Chain `.svc()`,
`.network()`, `.volume()`, then `.save()` to write or `.up()` to write
and start.

``` python
from fastops import Compose

dc = (Compose()
    .svc('db',
         image='postgres:16',
         env={'POSTGRES_PASSWORD': 'secret'},
         volumes={'pgdata': '/var/lib/postgresql/data'})
    .svc('redis', image='redis:7-alpine')
    .svc('app',
         build='.',
         ports={8080: 8080},
         env={'DATABASE_URL': 'postgresql://postgres:secret@db/app'},
         depends_on=['db', 'redis'],
         networks=['web'])
    .network('web')
    .volume('pgdata'))

print(dc)
```

### [`appfile()`](https://Karthik777.github.io/fastops/compose.html#appfile) — standard Python webapp Dockerfile

A one-liner for the most common Dockerfile pattern: copy requirements,
pip install, copy source, expose port, run `main.py`.

``` python
from fastops import appfile

print(appfile(port=8080, image='python:3.12-slim'))
```

Use `Compose.load(path)` to round-trip an existing `docker-compose.yml`.
`DockerCompose(path)` wraps the CLI for running compose commands against
any file.

## Reverse Proxying with Caddy

The [`caddy`](https://Karthik777.github.io/fastops/caddy.html#caddy)
module generates a `Caddyfile` and returns service kwargs for
[`Compose.svc()`](https://Karthik777.github.io/fastops/compose.html#compose.svc).
Four topologies are supported, from simplest to most secure.

### Plain Caddy — auto-TLS, ports 80 and 443

``` python
from fastops import caddyfile, caddy
import tempfile

# What the Caddyfile looks like:
print(caddyfile('myapp.example.com', port=8080))
```

``` python
with tempfile.TemporaryDirectory() as tmp:
    dc = (Compose()
        .svc('app', build='.', networks=['web'])
        .svc('caddy', **caddy('myapp.example.com', port=8080,
                              conf=f'{tmp}/Caddyfile'))
        .network('web')
        .volume('caddy_data').volume('caddy_config'))
    print(dc)
```

### DNS-01 challenge — no port 80 required

Pass `dns='cloudflare'` or `dns='duckdns'` to use DNS-01 ACME. This lets
you get TLS certs on machines that have port 80 blocked.

``` python
print(caddyfile('myapp.example.com', port=8080, dns='cloudflare', email='me@example.com'))
```

### Cloudflare tunnel — zero open ports

With `cloudflared=True`, Caddy listens on plain HTTP and cloudflared
tunnels traffic in. No ports need to be open on the host at all.

``` python
from fastops import cloudflared_svc

with tempfile.TemporaryDirectory() as tmp:
    dc = (Compose()
        .svc('app', build='.', networks=['web'])
        .svc('caddy', **caddy('myapp.example.com', port=8080,
                              cloudflared=True, conf=f'{tmp}/Caddyfile'))
        .svc('cloudflared', **cloudflared_svc(), networks=['web'])
        .network('web')
        .volume('caddy_data').volume('caddy_config'))
    print(dc)
```

### Full security stack — CrowdSec + Cloudflare tunnel

Add `crowdsec=True` to wire in the CrowdSec intrusion-detection bouncer.
The image is selected automatically based on the combination of
[`crowdsec`](https://Karthik777.github.io/fastops/caddy.html#crowdsec)
and `dns`.

``` python
from fastops import crowdsec

with tempfile.TemporaryDirectory() as tmp:
    dc = (Compose()
        .svc('app', build='.', networks=['web'])
        .svc('caddy', **caddy('myapp.example.com', port=8080,
                              crowdsec=True, cloudflared=True,
                              conf=f'{tmp}/Caddyfile'))
        .svc('crowdsec', **crowdsec())
        .svc('cloudflared', **cloudflared_svc(), networks=['web'])
        .network('web')
        .volume('caddy_data').volume('caddy_config')
        .volume('crowdsec-db').volume('crowdsec-config'))
    print(dc)
```

### Caddy image selection

| [`crowdsec`](https://Karthik777.github.io/fastops/caddy.html#crowdsec) | `dns` | Image |
|----|----|----|
| False | None | `caddy:2` |
| True | None | `serfriz/caddy-crowdsec:latest` |
| False | `'cloudflare'` | `serfriz/caddy-cloudflare:latest` |
| True | `'cloudflare'` | `ghcr.io/buildplan/csdp-caddy:latest` |
| False | `'duckdns'` | `serfriz/caddy-duckdns:latest` |

## SWAG (nginx alternative)

If you prefer [LinuxServer
SWAG](https://docs.linuxserver.io/general/swag/) (nginx + Certbot), use
[`swag()`](https://Karthik777.github.io/fastops/compose.html#swag). It
generates an nginx site-conf and returns service kwargs for
[`Compose.svc()`](https://Karthik777.github.io/fastops/compose.html#compose.svc).

``` python
from fastops import swag, swag_conf

# nginx site-conf for proxying to app:8080
print(swag_conf('myapp.example.com', port=8080))
```

``` python
with tempfile.TemporaryDirectory() as tmp:
    dc = (Compose()
        .svc('app', build='.', networks=['web'])
        .svc('swag', **swag('myapp.example.com', port=8080,
                            conf_path=f'{tmp}/proxy.conf'))
        .network('web')
        .volume('swag_config'))
    print(dc)
```

## Local Linux VMs with Multipass

The `multipass` module wraps the [Multipass](https://multipass.run) CLI
for spinning up ephemeral Ubuntu VMs. Ideal for testing deployment
scripts locally — no cloud account needed.

### cloud_init_yaml — VM bootstrap config

``` python
from fastops import cloud_init_yaml

# Default: Docker pre-installed
print(cloud_init_yaml(docker=True, packages=['htop', 'tree']))
```

### launch_docker_vm — the one-liner

[`launch_docker_vm()`](https://Karthik777.github.io/fastops/multipass.html#launch_docker_vm)
is the convenience wrapper that pairs
[`cloud_init_yaml`](https://Karthik777.github.io/fastops/multipass.html#cloud_init_yaml)
with
[`launch`](https://Karthik777.github.io/fastops/multipass.html#launch).
It covers the most common use case: spin up a clean Ubuntu VM with
Docker ready to go.

``` python
from fastops import launch_docker_vm, vm_ip, exec_, transfer, delete, vms

# Spin up an Ubuntu VM with Docker pre-installed (takes ~60s first run)
vm = launch_docker_vm('test-vm', cpus=2, memory='2G')

print(vms(running=True))             # ['test-vm']
print(vm_ip('test-vm'))              # '192.168.64.5'

exec_('test-vm', 'docker', 'ps')    # run any command in the VM
transfer('./docker-compose.yml',
         'test-vm:/home/ubuntu/docker-compose.yml')  # copy files

delete('test-vm')                    # purge when done
```

[`launch()`](https://Karthik777.github.io/fastops/multipass.html#launch)
accepts `cloud_init` as either a YAML string or a path to an existing
file — if it’s a string it writes a temp file, passes `--cloud-init`,
and cleans up automatically.

For raw CLI access use the `mp` singleton (same pattern as `dk`):

``` python
from fastops import mp
mp.info("test-vm")            # → multipass info test-vm
mp.snapshot("test-vm", name="before-deploy")
```

## Cloudflare DNS and Tunnels

The `cloudflare` module wraps the official [Cloudflare Python
SDK](https://github.com/cloudflare/cloudflare-python) for managing DNS
records and Zero Trust tunnels. Set `CLOUDFLARE_API_TOKEN` in your
environment.

``` sh
pip install "fastops[cloudflare]"
# or: pip install cloudflare
```

### dns_record — the one-liner

[`dns_record()`](https://Karthik777.github.io/fastops/cloudflare.html#dns_record)
is the convenience wrapper: reads `CLOUDFLARE_API_TOKEN` from env, looks
up the zone, deletes any existing record with the same name and type,
then creates the new one.

``` python
from fastops import dns_record, CF

# Point myapp.example.com at a server IP
record = dns_record('example.com', 'myapp', '1.2.3.4', proxied=True)

# Full control via CF() for multi-step workflows
cf = CF()  # reads CLOUDFLARE_API_TOKEN

# DNS
zid = cf.zone_id('example.com')
records = cf.dns_records(zid)
cf.delete_record(zid, records[0]['id'])

# Tunnels
tunnel = cf.create_tunnel('myapp-prod')
token  = cf.tunnel_token(tunnel['id'])
# → pass token as CF_TUNNEL_TOKEN in your environment
```

## App Dockerfiles

The `apps` module generates complete, production-ready Dockerfiles for
common stacks. All variants support uv-based installs, extra apt
packages, and optional healthchecks.

### Python / FastHTML

``` python
from fastops import python_app, fasthtml_app

# Generic single-stage Python app
print(python_app(port=8080, cmd=['uvicorn', 'main:app', '--host', '0.0.0.0']))
```

``` python
# FastHTML shortcut: uv + port 5001 + sensible defaults
print(fasthtml_app(port=5001, pkgs=['rclone'], volumes=['/app/data']))
```

### FastAPI + React (two-stage)

``` python
from fastops import fastapi_react

# Stage 1: Node builds the frontend; Stage 2: Python serves the API
print(fastapi_react(port=8000, frontend_dir='frontend'))
```

### Go and Rust (two-stage → distroless)

``` python
from fastops import go_app, rust_app

print(go_app(port=8080, go_version='1.22'))
print()
print(rust_app(port=8080, binary='myapp'))
```

### Cache mounts for faster rebuilds

`run_mount()` adds `RUN --mount=type=cache,...` to any instruction,
keeping pip/uv/cargo/go caches across builds:

``` python
df = (Dockerfile().from_('python:3.12-slim')
    .run_mount('uv sync --frozen --no-dev', target='/root/.cache/uv')
    .run_mount('go mod download',           target='/go/pkg/mod'))
```

## VPS Provisioning

The `vps` module covers the full lifecycle from a blank cloud server to
a running Compose stack: cloud-init generation, Hetzner provisioning,
and SSH-based deployment. No cloud SDK required beyond the `hcloud` CLI
and `ssh`/`rsync`.

### vps_init — cloud-init YAML

``` python
from fastops import vps_init

# Full bootstrap: UFW, deploy user, Docker, Cloudflare tunnel
yaml = vps_init(
    'prod-01',
    pub_keys='ssh-rsa AAAA...',
    docker=True,
    packages=['git', 'htop'],
)
print(yaml[:500])
```

### Hetzner provisioning

[`create()`](https://Karthik777.github.io/fastops/vps.html#create) wraps
`hcloud server create`. Pass the cloud-init YAML string directly — it
handles the temp-file lifecycle automatically.

``` python
from fastops import create, servers, server_ip, delete

ip = create(
    'prod-01',
    image='ubuntu-24.04',
    server_type='cx22',
    location='nbg1',
    cloud_init=yaml,
    ssh_keys=['my-laptop'],
)
print(servers())   # [{'name': 'prod-01', 'ip': '...', 'status': 'running'}]
```

Requires `hcloud` CLI and `HCLOUD_TOKEN` in env.

### deploy — sync and start

[`deploy()`](https://Karthik777.github.io/fastops/vps.html#deploy)
accepts a
[`Compose`](https://Karthik777.github.io/fastops/compose.html#compose)
object or a raw YAML string, rsyncs it to the server, and runs
`docker compose up -d`:

``` python
from fastops import deploy

deploy(dc, ip, user='deploy', key='~/.ssh/id_ed25519', path='/srv/myapp')
# 1. mkdir -p /srv/myapp  (via SSH)
# 2. rsync docker-compose.yml → prod-01:/srv/myapp/
# 3. docker compose up -d  (via SSH)
```

For one-off commands use
[`run_ssh()`](https://Karthik777.github.io/fastops/vps.html#run_ssh):

``` python
from fastops import run_ssh

print(run_ssh(ip, 'docker ps', user='deploy', key='~/.ssh/id_ed25519'))
```

## End-to-End: Deploy a Python App

Here is the complete workflow for deploying a Python webapp with Caddy
TLS, a Cloudflare tunnel, and CrowdSec — starting from a blank Python
file.

**Step 1: generate the configs** (pure Python, no daemon needed)

``` python
from fastops import Dockerfile
from fastops import Compose, appfile
from fastops import caddy, cloudflared_svc, crowdsec
import tempfile

DOMAIN = 'myapp.example.com'
PORT   = 8080

# Standard Python app Dockerfile
df = appfile(port=PORT)

with tempfile.TemporaryDirectory() as tmp:
    dc = (Compose()
        .svc('app',         build='.',  networks=['web'])
        .svc('caddy',       **caddy(DOMAIN, port=PORT,
                                   crowdsec=True, cloudflared=True,
                                   conf=f'{tmp}/Caddyfile'))
        .svc('crowdsec',    **crowdsec())
        .svc('cloudflared', **cloudflared_svc(), networks=['web'])
        .network('web')
        .volume('caddy_data').volume('caddy_config')
        .volume('crowdsec-db').volume('crowdsec-config'))

    print('--- Dockerfile ---')
    print(df)
    print('\n--- docker-compose.yml ---')
    print(dc)
```

**Step 2: save and deploy** (requires Docker daemon)

``` python
df.save('Dockerfile')
dc.save('docker-compose.yml')
dc.up()  # writes file + runs docker compose up -d
```

**Step 3: wire up DNS** (requires `CLOUDFLARE_API_TOKEN`)

``` python
from fastops import dns_record

dns_record('example.com', 'myapp', '1.2.3.4', proxied=True)
```

**Step 4: test locally first** (requires Multipass)

``` python
from fastops import launch_docker_vm, vm_ip, exec_, transfer, delete
from fastops import dns_record

vm = launch_docker_vm('test-vm')
transfer('./docker-compose.yml', 'test-vm:/home/ubuntu/')
exec_('test-vm', 'docker', 'compose', 'up', '-d')

ip = vm_ip('test-vm')
dns_record('example.com', 'myapp', ip)  # point DNS at the VM

delete('test-vm')  # clean up
```

**Step 5: provision and deploy to a real server** (requires `hcloud`
CLI + `HCLOUD_TOKEN`)

``` python
from fastops import vps_init, create, deploy
from fastops import dns_record

# Bootstrap a fresh Hetzner server
yaml = vps_init('prod-01', pub_keys=open('~/.ssh/id_ed25519.pub').read(),
                docker=True)
ip = create('prod-01', server_type='cx22', location='nbg1',
            cloud_init=yaml, ssh_keys=['my-laptop'])

# Point DNS at it
dns_record('example.com', 'myapp', ip, proxied=True)

# Sync Compose stack and start
deploy(dc, ip, key='~/.ssh/id_ed25519', path='/srv/myapp')
```

## Podman Support

Set `DOCKR_RUNTIME=podman` to switch all CLI calls to podman. The
generated Dockerfiles and Compose YAML are runtime-agnostic.

``` sh
export DOCKR_RUNTIME=podman
```

Credential-stripping
([`_clean_cfg()`](https://Karthik777.github.io/fastops/core.html#_clean_cfg))
is skipped automatically for non-docker runtimes.
