Skip to content

Python API

Everything the gc-batch CLI does is also available as a Python library, so you can submit and monitor Batch jobs from a script, a notebook, or your own orchestration code without shelling out to the CLI.

The pieces you will use most:

Object Purpose
GCBatchClient Create, list, inspect, and cancel jobs
BatchClientConfig Project, location, and deployment settings for the client
BatchJobConfig Machine type, disks, mounts, networking, environment
JobRequest One job: name, image, command, config, labels
GCBatchSettings Deployment values (job-name prefix, job profiles)
BatchLogging Read job logs from Cloud Logging
GCSLogReader Read job logs written to a GCS bucket

Submitting a job

from gc_batch import BatchClientConfig, BatchJobConfig, GCBatchClient, JobRequest

client = GCBatchClient(BatchClientConfig(project_id="my-project", location="us-central1"))

job = client.create_job(
    JobRequest(
        job_name="hello-world",
        docker_image="python:3.12-slim",
        command="python -c 'print(\"Hello from Batch\")'",
        config=BatchJobConfig(
            machine_type="e2-standard-2",
            boot_disk_type="pd-balanced",
        ),
    )
)

print(job.name)  # projects/my-project/locations/us-central1/jobs/hello-world-1758412800

machine_type and boot_disk_type are the only required fields on BatchJobConfig. Disk-type support varies by machine generation, so let MachineTypeHelper pick one if you are generating configs programmatically:

from gc_batch import MachineTypeHelper

machine_type = "c4-standard-8"
config = BatchJobConfig(
    machine_type=machine_type,
    boot_disk_type=MachineTypeHelper.get_default_disk_type(machine_type),
)

Job names get a timestamp

create_job appends a Unix timestamp to job_name (and prepends settings.job_name_prefix when configured), so the submitted name is not the one you passed. Read the real name off the returned job — either job.name for the full resource path or job.name.split("/")[-1] for the short name that get_job and the CLI expect.

Labels

create_job always sets four labels: job-name, created-using (from settings.created_using_label), created-by, and created-at. Anything in JobRequest.labels is merged on top, so an explicit label wins over the default.

request = JobRequest(
    job_name="nightly-rollup",
    docker_image="gcr.io/my-project/rollup:latest",
    command="python /app/rollup.py",
    config=config,
    labels={"team": "data-science", "environment": "production"},
)

created-by is what gc-batch list-my-jobs filters on. It is resolved from the first environment variable in settings.owner_email_env_vars that is set ($OWNER_EMAIL, $WORKBENCH_USER_EMAIL, $TERRA_USER_EMAIL, then $USER by default), reduced to the local part of an email address — so jane.doe@example.com becomes jane-doe, and it falls back to unknown when none are set. Jobs you submit from the library are therefore findable with list-my-jobs just like CLI-submitted ones.

For code that runs unattended, where no user identity applies, set it explicitly:

labels = {"team": "data-science", "created-by": "nightly-pipeline"}

You can resolve the same value yourself, which is how you would build a list_jobs filter that matches the current user:

from gc_batch.utils import resolve_created_by_label

created_by = resolve_created_by_label(client.config.settings.owner_email_env_vars)
my_jobs = client.list_jobs(labels={"created-by": created_by})

Label values must match GCP's rules: lowercase letters, numbers, -, and _.

Waiting for a job to finish

There is no blocking wait call; poll get_job and check the state with is_job_finished:

import time

from gc_batch.utils import is_job_finished

short_name = job.name.split("/")[-1]

while True:
    job = client.get_job(short_name)
    if is_job_finished(job):
        break
    time.sleep(30)

print(job.status.state.name)  # SUCCEEDED, FAILED, CANCELLED, ...

if job.status.state.name == "FAILED":
    print(client.get_failure_message(job))

get_failure_message summarizes the state, the most relevant failure event, the failed-task count, and the job UID — the same summary gc-batch status --full prints.

Listing and filtering jobs

from datetime import datetime, timedelta, timezone

# Everything in the project/location
all_jobs = client.list_jobs()

# By label
team_jobs = client.list_jobs(labels={"team": "data-science"})

# Failed in the last day
recent_failures = client.list_jobs(
    status="FAILED",
    since_time=datetime.now(timezone.utc) - timedelta(days=1),
)

# Combined filters
jobs = client.list_jobs(
    labels={"team": "data-science", "environment": "production"},
    status="RUNNING",
    page_size=500,
)

for job in jobs:
    print(job.name.split("/")[-1], job.status.state.name)

labels, status, and since_time are pushed into the Batch API filter. name is applied client-side (the Batch API has no name filter), so it narrows an already-fetched page rather than reducing what is fetched.

Cancelling a job

cancel_job takes the full resource path, not the short name:

job = client.get_job("nightly-rollup-1758412800")
client.cancel_job(job.name)

Logs

A job's logs live in one of two places, fixed when the job is created: Cloud Logging by default, or a GCS bucket when logs_bucket was set. Each has a reader on the client — client.batch_logging for Cloud Logging and client.gcs_logging for a bucket — and both expose the same print_logs_for_job / download_logs_for_job methods:

job = client.get_job("nightly-rollup-1758412800")

# Print to stdout
client.batch_logging.print_logs_for_job(job)

# Only warnings and above
client.batch_logging.print_logs_for_job(job, severity="WARNING")

# Write <job-name>.log into a directory
client.batch_logging.download_logs_for_job(job, download_dir="./logs")

# A Cloud Console link, handy to put in a Slack message or a dashboard
print(client.get_cloud_logging_url(job, severity="ERROR"))

Both readers return the number of entries handled, so zero is a successful read of an empty log rather than a failure, and both raise on an API error instead of printing it — see Errors.

To handle a job either way, ask which route it uses. This is exactly what gc-batch logs print does:

from gc_batch.gcs_logging import GCSLogReader

reader = client.gcs_logging if GCSLogReader.uses_gcs_logs(job) else client.batch_logging
entry_count = reader.print_logs_for_job(job)
if entry_count == 0:
    print("The job produced no log entries.")

For the entries themselves rather than printed output, client.gcs_logging has read_logs_for_job, which returns dicts with timestamp, severity, textPayload, jsonPayload, and resource keys:

for entry in client.gcs_logging.read_logs_for_job(job, severity="ERROR"):
    print(entry["timestamp"], entry["textPayload"])

# Where the logs actually are, without reading them
print(GCSLogReader.resolve_log_location(job).uri)  # gs://my-bucket/batch-logs/<job>/

Entries come back in the order Batch wrote them — objects within a task, tasks in index order, stdout before stderr — not re-sorted by timestamp, because Batch timestamps have only one-second resolution.

Choosing the destination

Set logs_bucket to route logs to GCS. Batch supports a single destination, so this turns Cloud Logging off for that job:

config = BatchJobConfig(
    machine_type="e2-standard-2",
    boot_disk_type="pd-balanced",
    logs_bucket="my-bucket/batch-logs",
)

Logs then land under gs://my-bucket/batch-logs/<timestamped-job-name>/, and client.gcs_logging reads them. This is the route to use where the caller has no Cloud Logging read access at all — a restricted-VPC environment, where querying Cloud Logging returns 403 Permission denied for all log views. Since the destination cannot be changed after creation, decide before submitting.

Mounts, local SSD, and environment

BatchJobConfig covers the same ground as the create flags:

config = BatchJobConfig(
    machine_type="n2-standard-8",
    boot_disk_type="pd-balanced",
    boot_disk_size=100,
    # GCS buckets mounted into the container (no gs:// prefix)
    input_bucket="my-bucket/datasets",
    input_dir="/mnt/input",
    output_bucket="my-bucket/results",
    output_dir="/mnt/output",
    # Scratch space; size must be a multiple of 375 GB
    local_ssd_size_gb=375,
    local_ssd_mount_path="/mnt/scratch",
    # SPOT VMs for non-urgent work
    provisioning_model="SPOT",
    # Environment variables for the container
    user_env_dict={"LOG_LEVEL": "DEBUG", "MAX_WORKERS": "4"},
)

request = JobRequest(
    job_name="analysis",
    docker_image="gcr.io/my-project/analyzer:latest",
    command="python /app/analyze.py",
    args="--input /mnt/input/data.csv --output /mnt/output/results/",
    config=config,
)

For requester-pays buckets, set input_billing_project / logs_billing_project. LSSD machine types (for example c4-standard-8-lssd) come with SSDs attached, so set local_ssd_mount_path but leave local_ssd_size_gb unset — passing both raises ValueError.

See Input/Output Mounts and Local SSD Storage for the details behind these fields.

Settings and job profiles

BatchClientConfig.settings holds the deployment-specific values. Constructing GCBatchSettings directly uses environment variables and defaults only; GCBatchSettings.load() also reads the TOML config file:

from gc_batch import BatchClientConfig, GCBatchClient, GCBatchSettings

settings = GCBatchSettings.load()  # ./gc-batch.toml, $GC_BATCH_CONFIG_FILE, XDG path
client = GCBatchClient(
    BatchClientConfig(
        project_id=settings.default_project_id or "my-project",
        location="us-central1",
        settings=settings,
    )
)

Or set them inline, skipping config-file discovery entirely:

settings = GCBatchSettings(
    job_name_prefix="team-a-",
    created_using_label="my-pipeline",
)

Job profiles are bundles of networking/VM settings — what the CLI applies for --job-profile. In library code, apply one with BatchJobConfig.apply_profile:

from gc_batch import BatchJobConfig, GCBatchSettings

settings = GCBatchSettings.load()
profile = settings.job_profiles["all-of-us"]  # built-in; add your own via config

config = BatchJobConfig(
    machine_type="n2-standard-4",
    boot_disk_type="pd-balanced",
).apply_profile(profile)

apply_profile mutates the config in place and returns it, so it works either chained (as above) or as a statement on an existing config. Fields the profile leaves unset are untouched, so you can apply a profile over a config that already has other settings. When the profile sets service_account_from_gcloud, the service account is resolved from the active gcloud account and a ValueError is raised if there isn't one:

try:
    config.apply_profile(profile)
except ValueError as error:
    print(f"{error}")  # ... Please run `gcloud auth login`.

A profile's cloud_logging_unreadable flag is advisory rather than a job setting, so apply_profile does not act on it. It marks environments where Cloud Logging cannot be read; the CLI warns when such a profile is used without --logs-bucket, and library code can make the same check:

if profile.cloud_logging_unreadable and not config.logs_bucket:
    raise RuntimeError("Set logs_bucket, or this job's logs will be unreadable.")

See Configuration for defining your own profiles.

Fanning out over many inputs

Submission is cheap and non-blocking, so the usual pattern is to submit a job per input and then poll the batch of them:

import time

from gc_batch import BatchClientConfig, BatchJobConfig, GCBatchClient, JobRequest
from gc_batch.utils import is_job_finished

SAMPLES = ["SAMPLE_001", "SAMPLE_002", "SAMPLE_003"]

client = GCBatchClient(BatchClientConfig(project_id="my-project", location="us-central1"))

submitted = []
for sample in SAMPLES:
    config = BatchJobConfig(
        machine_type="n2-highmem-8",
        boot_disk_type="pd-balanced",
        boot_disk_size=500,
        input_bucket=f"my-genomics-bucket/raw-samples/{sample}",
        output_bucket=f"my-genomics-bucket/processed/{sample}",
    )
    job = client.create_job(
        JobRequest(
            job_name=f"sequence-{sample.lower().replace('_', '-')}",
            docker_image="gcr.io/my-project/sequencing:latest",
            command="python /app/sequence.py",
            args=f"--sample-id {sample} --reference hg38",
            config=config,
            labels={"project": "genomics", "sample": sample.lower().replace("_", "-")},
        )
    )
    submitted.append(job.name.split("/")[-1])

pending = set(submitted)
results = {}
while pending:
    for short_name in list(pending):
        job = client.get_job(short_name)
        if is_job_finished(job):
            results[short_name] = job.status.state.name
            pending.discard(short_name)
    if pending:
        time.sleep(30)

failed = [name for name, state in results.items() if state != "SUCCEEDED"]
print(f"{len(results) - len(failed)}/{len(results)} succeeded")

Since each submitted job carries your labels, you can also poll the whole set with one API call instead of one per job:

jobs = client.list_jobs(labels={"project": "genomics"}, status="RUNNING")
print(f"{len(jobs)} still running")

Errors

The client logs and re-raises the underlying google-cloud-batch exceptions, so handle them as you would any GCP API call. Configuration mistakes surface earlier, as pydantic.ValidationError when the models are constructed:

from google.api_core import exceptions as gcp_exceptions
from pydantic import ValidationError

try:
    config = BatchJobConfig(machine_type="n2-standard-4", boot_disk_type="not-a-disk")
except ValidationError as error:
    print(f"Bad job config: {error}")

try:
    job = client.create_job(request)
except gcp_exceptions.PermissionDenied as error:
    print(f"Check your credentials and project: {error}")

Log retrieval raises too, rather than reporting a failure as an empty log, so a permission problem is distinguishable from a job that printed nothing:

try:
    entry_count = client.batch_logging.print_logs_for_job(job)
except gcp_exceptions.PermissionDenied:
    # Normal in restricted environments; such jobs need logs_bucket instead.
    print("No Cloud Logging read access in this project.")
else:
    if entry_count == 0:
        print("The job produced no log entries.")

client.gcs_logging additionally raises ValueError if the job does not write its logs to a bucket, which is what GCSLogReader.uses_gcs_logs(job) checks for.

Pass your own logger if you want the client's output to go through your application's logging setup:

import logging

client = GCBatchClient(
    BatchClientConfig(project_id="my-project", location="us-central1"),
    log_level=logging.DEBUG,
)

Full API reference

The generated reference documents every model field and method: