Skip to main content

Like `subprocess.run`, but with the ability to pipe `stdout` and/or `stderr` to a `Logger` or capture each stream independently while the process is running.

Project description

run_with_logger

Like subprocess.run, but with the ability to pipe stdout and/or stderr to a Logger or capture each stream independently while the process is running.

What it can do

  • Run local commands.
  • Run remote commands over SSH.
  • For each output stream (stdout and stderr), choose independently whether to log, capture, or discard it.
  • Pipe bytes or files to stdin.
  • Pass environment variables to the subprocess (even via SSH if the server's AcceptEnv setting allows it).
  • Enforce configurable timeouts for local subprocesses, remote SSH commands, and SSH network operations.
  • Support many of the same arguments as subprocess.run.
  • Provide access to the running process and incrementally captured output via a context manager.

Examples

Log stderr and capture stdout

The main process will wait for the subprocess while:

  • forwarding stderr line by line to the logger
  • capturing stdout in a CompletedProcess object, which is returned when the process finishes
from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["foo", "bar"],
    logger=logger,
    stdout_action="capture",
    stderr_action="log",
    check=False,
)

print(completed.stdout)

Log stdout and discard stderr

The main process will wait for the subprocess while:

  • forwarding stdout line by line to the logger
  • discarding stderr
from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["foo", "bar"],
    logger=logger,
    stdout_action="log",
    stderr_action="discard",
    check=False,
)

Capture both streams

Use "capture" for both streams when you want the final CompletedProcess to contain bytes from stdout and stderr.

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["python", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
    logger=logger,
    stdout_action="capture",
    stderr_action="capture",
)

print(completed.stdout.decode().strip())  # "out"
print(completed.stderr.decode().strip())  # "err"

Choose the log level and decoding

from logging import INFO, getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

run_with_logger(
    args=["python", "-c", "print('hello')"],
    logger=logger,
    level=INFO,
    encoding="utf-8",
    stdout_action="log",
    stderr_action="discard",
)

Send bytes to stdin

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["python", "-c", "import sys; print(sys.stdin.read().upper())"],
    logger=logger,
    stdin_data=b"hello",
    stdout_action="capture",
    stderr_action="capture",
)

print(completed.stdout.decode().strip())  # "HELLO"

Pipe a file to stdin

from logging import getLogger
from pathlib import Path
from run_with_logger import run_with_logger

logger = getLogger(__name__)
input_path = Path("input.txt")

with input_path.open("rb") as stdin:
    completed = run_with_logger(
        args=["python", "-c", "import sys; print(sys.stdin.read())"],
        logger=logger,
        stdin_io=stdin,
        stdout_action="capture",
        stderr_action="capture",
    )

Add environment variables

extra_env is merged with the current environment.

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["python", "-c", "import os; print(os.environ['APP_MODE'])"],
    logger=logger,
    extra_env={"APP_MODE": "production"},
    stdout_action="capture",
    stderr_action="capture",
)

print(completed.stdout.decode().strip())  # "production"

Use a shell command

Like subprocess.Popen, pass a string and shell=True when you intentionally want shell parsing.

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args="printf 'hello from the shell\n'",
    shell=True,
    logger=logger,
    stdout_action="capture",
    stderr_action="capture",
)

Handle non-zero exit codes

With the default check=True, non-zero exits raise CalledProcessError. Captured streams are attached to the exception.

from logging import getLogger
from subprocess import CalledProcessError
from run_with_logger import run_with_logger

logger = getLogger(__name__)

try:
    run_with_logger(
        args=["python", "-c", "import sys; print('bad'); sys.exit(2)"],
        logger=logger,
        stdout_action="capture",
        stderr_action="capture",
    )
except CalledProcessError as error:
    print(error.returncode)       # 2
    print(error.output.decode())  # captured stdout

Set check=False when you want to inspect the return code yourself:

from logging import getLogger
from run_with_logger import run_with_logger

logger = getLogger(__name__)

completed = run_with_logger(
    args=["python", "-c", "import sys; print('bad'); sys.exit(2)"],
    logger=logger,
    stdout_action="capture",
    stderr_action="capture",
    check=False,
)
print(completed.returncode)       # 2
print(completed.stdout.decode())  # captured stdout

Inspect a running process

Use run_with_logger__cm if you need access to the Popen object while the process is still running. Captured streams are available as BytesIO buffers.

import time
from io import BytesIO
from logging import getLogger
from run_with_logger import run_with_logger__cm

logger = getLogger(__name__)

with run_with_logger__cm(
    args=["python", "-uc", "import time; print('one'); time.sleep(1); print('two')"],
    logger=logger,
    stdout_action="capture",
    stderr_action="capture",
) as info:
    stdout_buffer = info["stdout_buffer"]
    assert isinstance(stdout_buffer, BytesIO)

    while info["process"].poll() is None:
        partial_stdout = stdout_buffer.getvalue()
        # Do something with partial_stdout while the process runs.
        time.sleep(0.1)

completed = info["completed"]

Run a remote command over SSH

Install SSH support first:

python -m pip install "run_with_logger[ssh]"

Then pass either a connected paramiko.SSHClient or a fabric.Connection.

from contextlib import closing
from logging import getLogger
from paramiko import SSHClient
from run_with_logger import run_with_logger__ssh

logger = getLogger(__name__)
with closing(SSHClient()) as client:
    
    # Configure and connect the client as appropriate for your environment.
    
    completed = run_with_logger__ssh(
        client=client,
        command="whoami",
        logger=logger,
        stdout_action="capture",
        stderr_action="capture",
    )

print(completed.stdout.decode().strip())

run_with_logger__ssh__cm provides the same context-manager pattern for SSH channels.

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

run_with_logger-1.5.0.tar.gz (22.9 kB view details)

Uploaded Source

Built Distribution

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

run_with_logger-1.5.0-py3-none-any.whl (14.7 kB view details)

Uploaded Python 3

File details

Details for the file run_with_logger-1.5.0.tar.gz.

File metadata

  • Download URL: run_with_logger-1.5.0.tar.gz
  • Upload date:
  • Size: 22.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for run_with_logger-1.5.0.tar.gz
Algorithm Hash digest
SHA256 5c345970272ae81ded9da6d824315e41f17f50b500211b1dccaa80036366d094
MD5 3589955985f13c7605acbd0d0d472bfd
BLAKE2b-256 5ecef8c4edcc8cc663aeda34d8f32a9ba9d89c14148af7f5ae5b5ee8ae0220b5

See more details on using hashes here.

Provenance

The following attestation bundles were made for run_with_logger-1.5.0.tar.gz:

Publisher: publish.yml on rudolfbyker/run_with_logger

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file run_with_logger-1.5.0-py3-none-any.whl.

File metadata

File hashes

Hashes for run_with_logger-1.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 56470d71cc0d2abd84fade5084960d7a39f7f49c3a957c49cdd9ad3f81e82839
MD5 0eb4c8eeb965c2a354e84b29932d7882
BLAKE2b-256 36ef35338c3117a74a49d8b9411f4e60eb3b44529e843624a08d0df7eb6779af

See more details on using hashes here.

Provenance

The following attestation bundles were made for run_with_logger-1.5.0-py3-none-any.whl:

Publisher: publish.yml on rudolfbyker/run_with_logger

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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