Skip to content

API cheatsheet

A flat, greppable reference of firm's public API — return types are shown in # -> comments, and keyword-only parameters are simply listed by name. See the Cookbook for worked, runnable examples.

firm.queue

import firm.queue as bq
from firm.queue import current_runtime

# bq.configure(database_url, ...) -> Runtime (process-global). All but database_url are keyword-only;
# pass engine=<your SQLAlchemy engine> instead of a URL to share your app's engine/pool:
bq.configure("postgresql://localhost/app", busy_timeout_ms=5000,
             pool_size=20, max_overflow=40,
             default_queue="default", preserve_finished_jobs=True)
current_runtime()        # -> Runtime


@bq.job(queue="default", priority=0, attempts=1, backoff=3.0, concurrency=None)
def my_job(x):           # the function stays directly callable
    ...


my_job.enqueue(x)                    # -> int | None  (job id, or None if discarded)
my_job.enqueue_at(when, x)           # when: datetime
my_job.enqueue_in(delta, x)          # delta: timedelta

Concurrency (the concurrency= dict on @bq.job):

concurrency = {
    "key": lambda *a, **k: a[0],  # the variable part of the concurrency key
    "to": 1,                      # max simultaneous executions per key
    "duration": 300,              # seconds before the semaphore expires (failsafe)
    "group": None,                # share a key namespace across jobs (default: this job)
    "on_conflict": "block",       # "block" (queue it) or "discard" (drop it)
}

Management (from firm.queue import queues), each takes a Runtime:

from firm.queue import queues

queues.all_queues(rt)    # -> list[str]
queues.size(rt, q)       # -> int
queues.pause(rt, q)
queues.resume(rt, q)
queues.is_paused(rt, q)  # -> bool
queues.clear(rt, q)      # -> int  (jobs removed)
queues.latency(rt, q)    # -> float  (seconds since the oldest ready job)

Maintenance (from firm.queue import maintenance):

from datetime import timedelta
from firm.queue import maintenance

maintenance.retry_failed(rt, job_id)                          # -> bool
maintenance.retry_all_failed(rt)                              # -> int
maintenance.clear_finished(rt, older_than=timedelta(days=7), batch_size=500)  # -> int

Running workers:

# CLI (production):
#   firm-queue start --import myapp.jobs [--mode fork|thread] [--threads N] [--queues a,b]

from firm.queue import current_runtime
from firm.queue.supervisor import (ThreadSupervisor, ForkSupervisor, SupervisorConfig,
                                     WorkerConfig, DispatcherConfig)

config = SupervisorConfig(workers=[WorkerConfig(queues=("*",), threads=3)],
                          dispatchers=[DispatcherConfig()], recurring=[])
sup = ThreadSupervisor(current_runtime(), config)   # in-process; .start() is non-blocking
sup.start()
sup.stop()                                          # or:  with ThreadSupervisor(rt, config): ...
ForkSupervisor(current_runtime(), config).start()   # production default (POSIX); blocks

Single steps (no long-lived process):

from firm.queue.worker import run_ready
from firm.queue.dispatcher import dispatch_once, run_maintenance

run_ready(rt, queues=("*",), limit=10)    # -> int  (claim + run one batch of up to `limit`)
dispatch_once(rt)                          # -> int  (promote due scheduled jobs)
run_maintenance(rt)                         # -> int  (release blocked jobs with capacity)

Recurring (from firm.queue.scheduler import RecurringTask, Scheduler):

from firm.queue.scheduler import RecurringTask, Scheduler

# 5-field cron; pass a list as SupervisorConfig(recurring=[...]) to start a scheduler automatically
task = RecurringTask(key="nightly", schedule="0 3 * * *", job=my_job, args=(), kwargs={})
Scheduler(rt, [task]).tick()   # manual: enqueue anything due this period

firm.cache

from firm.cache import Cache, JSONCoder, PickleCoder

# database_url is positional; everything after it is keyword-only (defaults shown):
cache = Cache("sqlite:///cache.db", engine=None, coder=None, encrypt_key=None,
              max_age=1209600.0, max_size=268435456, max_entries=None,   # 2 weeks / 256 MiB / off
              expiry_batch_size=100, max_key_bytesize=1024, size_estimate_samples=10000,
              create_schema=True, auto_expire=True, background_expiry=False, expiry_interval=60.0)

cache.get(key)                      # -> Any | None
cache.set(key, value)
cache.fetch(key, default_or_callable)   # -> Any  (compute + store on a miss)
cache.delete(key)                   # -> bool
cache.exist(key)                    # -> bool
cache.get_multi(keys)               # -> dict
cache.set_multi(mapping)
cache.increment(key, by=1)          # -> int
cache.decrement(key, by=1)          # -> int
cache.clear()
cache.close()                       # or:  with Cache(...) as cache: ...
# keys are str|bytes; values are arbitrary (pickle default; JSONCoder for interop).
# encrypt_key=<Fernet key> encrypts values at rest (needs the [encryption] extra).

firm.channel

from firm.channel import Channel

channel = Channel("sqlite:///cable.db", engine=None, polling_interval=0.1,
                  message_retention=86400.0, auto_trim=True, trim_batch_size=100,
                  create_schema=True)

channel.broadcast(name, payload)    # name/payload: str|bytes (str payloads are UTF-8 encoded)
channel.subscribe(name, callback)   # callback(payload: bytes); only future messages; per-process
channel.unsubscribe(name, callback)
channel.trim()                      # -> int
channel.close()                     # or:  with Channel(...) as channel: ...

firm.audit

from firm.audit import AuditLog, Ref, record

# database_url is positional; everything after it is keyword-only (defaults shown):
audit = AuditLog("sqlite:///audit.db", engine=None, create_schema=True,
                 max_age=None, background_retention=False, retention_interval=3600.0)

audit.record(action, subject=None, actor=None, data=None, changes=None,
             correlation_id=None, context=None, conn=None)
audit.history(subject=None, subject_type=None, subject_id=None,
              actor=None, actor_type=None, actor_id=None, action=None,
              correlation_id=None, since=None, limit=100)   # -> list[dict]
# subject_type=/subject_id= filter independently (either alone, or both); same for actor_*.
# pass either subject= or subject_type=/subject_id= for a field, never both (raises ValueError).
# a bare-string filter (actor="cron") filters by type only.
audit.close()                       # or:  with AuditLog(...) as audit: ...

# shared-DB, same-transaction (atomic with the business change):
record(conn, action, subject=None, actor=None, data=None, changes=None,
       correlation_id=None, context=None)

# subject/actor accept the same forms — type and id are each optional:
#   invoice                     domain object with `.id`  -> (ClassName, id)
#   ("Invoice", 42)             tuple; either half may be None (empty/None id -> NULL)
#   "cron"                      bare string -> a role/kind label, stored as the type (no id)
#   Ref("User", 7, name="…")    explicit, with an optional human-readable display name
#   None                        no actor/subject (a system event)
# An object may define __firm_audit_ref__(self) -> Ref to customize its audit identity.
# Each event dict from history()/get() carries subject_label/actor_label (the display names).
# data/changes/context: dicts stored as JSON text (not native JSON/JSONB).

firm.contrib (optional — [flask] / [fastapi] extras)

# FastAPI: app = FastAPI(lifespan=lifespan(database_url=..., embed_workers=False,
#                                          queues=("*",), threads=3))
from firm.contrib.fastapi import lifespan

# Flask: Firm(app, database_url=None, embed_workers=False, queues=("*",), threads=3)
#   reads app.config["FIRM_DATABASE_URL"]; registers `flask firm worker`
from firm.contrib.flask import Firm

from firm.contrib.sqlalchemy import enqueue_after_commit
enqueue_after_commit(session, my_job, x)   # enqueues iff the session commits

Command-line tools

firm-queue start|work|drain|dispatch|maintenance --database-url ... --import myapp.jobs
firm-cache stats|clear|trim --database-url ...
firm-channel stats|trim --database-url ...
firm-audit stats|history|prune --database-url ...
firm-ui --database-url ... [--queue-url ... --cache-url ... --channel-url ... --audit-url ...] [--host --port]

Database URLs come from --database-url or the FIRM_*_DATABASE_URL env vars. Bare postgresql:// / mysql:// URLs auto-normalize to the shipped drivers ([postgres] / [mysql]).