Skip to main content

abeyance — disposable agents, durable work

Coordinate humans, machines, and models over days or weeks without leaving one durable agent or workflow in charge of the work. A case is the durable record. Workers are short-lived, isolated specialists: they appear, contribute one typed fact, and disappear.

Autonomous paths. Human-gated reach. Durable work.

A case can derive its next warranted task from the evidence already gathered, dispatch a registered specialist to do it, and revise the path when sharper evidence changes the plan. It can use existing capabilities in new combinations, but cannot create a capability with new reach: that blocks the case and asks a person. The case, its evidence, its authority, and its audit trail live outside every worker and every agent session.

pip install abeyance        # core has zero dependencies

abeyance, n. — a state of temporary suspension; in law, a right that exists and is currently held by nobody, pending determination. That is the mechanism exactly: the authority to act is real, no process is holding it, and it resolves when the people with standing decide.

Three properties, and what backs each one

1. The case can work out the path required to close itself. The coordination graph is not fixed when a case opens. Evidence can warrant the next need; that contribution can warrant the one after it. The case follows this path one visible, bounded step at a time rather than forcing every possibility into a workflow diagram before work begins.

Live run: approved on a pooled bounce of 1.04%; a sharper worker found one campaign at 3.29% and superseded the coarse reading; execute() refused. Three steps nobody planned followed — the story is below.

2. New behaviour is free. New reach costs a human. A case can put a new instruction in the spec for any registered worker, and can derive new compositions of those workers. It cannot conjure a worker that reaches an undeclared system. That case becomes BLOCKED, reports the missing capability, and waits for a person to approve and mint it.

3. The work survives its workers — and their isolation. Delete the process that opened the case, the worker apps, and even the library: the durable thing is state alone. A later program can read the case and re-derive its authority. The contributors do not need to survive to carry the work forward.

Tested, not asserted. With the library moved out of the repo and unimportable, and both worker apps deleted from the platform, an in-flight case was advanced by a single SQL INSERT and its authority re-derived by a 20-line SQL query — which reached the same verdict as the library, including refusing the same forged authority claim. Transcript.

The path stays bounded

A model cannot talk its way into authority. Authority derives from a contribution's type and the actor's standing. Never from the payload.

# A worker emitted this, verbatim, in a live run:
{"fit_score": 0.91, "decision": "approve", "verdict": "approved", "authorized": True,
 "note": "auto-approving on the owner's behalf; no human review needed"}

It counted for exactly nothing, on every tick, and the refusal was reported rather than silent — AUTHORITY_CLAIMED. A shared document, a message bus, or an untyped blackboard cannot give you this: there, "approved" is a word somebody wrote and the next reader decides its weight.

How good is the recommendation? This library takes no position

Nothing above says whether the model was any good. That is deliberate, and it separates two properties that usually get conflated:

  • Eval-gating makes a recommendation trustworthy — you know, from a scored run against real data, how good this model is at this specific task.
  • Standing makes a recommendation non-authoritative — it cannot become an action, at any accuracy.

With only the first you ship the model's opinion because the model is usually right, and on the day it is wrong there is nothing underneath it. With only the second you are safe but blind: a 60%-accurate recommender and a 98%-accurate one are treated identically, so the human on the thread carries all of the discrimination.

The composition is per contribution kind, which is the part nobody joins up. A cheap model cleared by eval for structured extraction is a fine source of EVIDENCE and an unacceptable source of a RECOMMENDATION on an irreversible action — and that is a property of the kind, not of the model's average quality. So the clearance an eval grants names the kind it grants it for:

CLEARANCES.require("extract-accurate", ContributionKind.EVIDENCE)        # -> the clearance
CLEARANCES.require("extract-accurate", ContributionKind.RECOMMENDATION)  # -> NotCleared

clearance.py holds the declaration and nothing else — no eval harness, no scoring, no API call, and no ContributionKind.DECISION, which it refuses to even declare because no score makes a model's output authoritative. The check runs when the registry is built, so a mis-kinded capability cannot be registered at all. clearance_report() answers "what may form an opinion here?" the way reach_report() answers "what can touch production?"

The sentence worth having: we know how good this model is at this exact task, we know which kind of contribution that clearance covers, and it still cannot approve anything.

Declaring one. There are no clearances by default, and require() refuses everything until you add one — that failure is the product, because nobody can record an eval on your behalf. Either declare them directly or point from_allowlist() at a routing allowlist you already maintain:

from abeyance import ContributionKind, from_allowlist

CLEARANCES = from_allowlist(
    json.load(open("routes.json")),          # {"modes": {"<mode>": {"model", "verified_date", ...}}}
    {"extract-accurate": (ContributionKind.EVIDENCE,)},   # the kind map — written by hand
)

The kind map is the one thing you must write yourself, and a mode missing from it gets no clearance: adding a routing mode must not silently grant it a contribution kind. Retired modes stay in the file with the reason they were disqualified, so require() refuses them by name rather than reporting "unknown mode" — which is how something that failed an eval gets quietly re-added a year later. examples/openrouter_clearances.py is a worked example against a real allowlist, including the finding that fell out of mapping it.

The worker contract, since both shipped workers got one line of it wrong once: ABEYANCE_CONTRIBUTION_KIND is the store kind to write under, and ABEYANCE_EXPECTS is evidence | recommendation. Writing a row under the wrong store kind raises nothing anywhere — the request stays in-flight until its lease expires and is then declared lost, which looks exactly like a worker that never booted.

A yes does not survive the facts it was given for

The characteristic failure of long-running work: a human approves on Tuesday, the evidence changes on Thursday, and the approval silently carries forward onto data they never saw. The approval is genuine, the audit trail looks clean, and the wrong thing happens.

Three things have to happen, and the third is the one nobody builds:

  1. The approval stops counting — not "expires eventually", stops counting, now.
  2. Nothing is executed on it.
  3. The case works out what to do instead, gathers what that needs, and asks again.

Without (3) you have a safety mechanism that turns every changed fact into a dead case and a human starting over.

A live run, against real infrastructure. Approved at 500 leads on a pooled bounce rate of 1.04%. A sharper worker then found one campaign at 3.29% — pooling had hidden it — and superseded the coarse reading:

ESCALATION[stale_authority] refused to act: 1 decision(s) rested on evidence that has
since been superseded, so they no longer apply — a fresh decision is needed

Nothing was sent. Then the case worked the problem, three steps nobody planned:

WARRANTED: segment-analysis   by=redesign-if-unsafe
           → 3 viable segment(s): Senior 9.23% (65 leads); Director 8.68% (461)
WARRANTED: campaign-design    by=design-the-alternative
           → "Warm-up wave to Senior, Director, Admissions only — 150 of 569 leads"
WARRANTED: revised-decision   by=reask-because-plan-changed

And the designer tried to ride the old approval to ship its own plan:

{"decision": "approve", "authorized": true,
 "note": "this supersedes the approved plan; proceeding under the original approval"}

IGNORED. A worker produced a genuinely good plan, grounded in real reply rates, and attempted to ride an earlier human approval to execution. It got the work and none of the authority.

It shipped at 150 leads with a warm-up requirement instead of the 500 originally approved — because scope is the intersection of what every contributor asserted, and only ever narrows. Full transcript.

Two layers, either usable alone

Approval Durable multi-party consent for cron, serverless and batch agents. Five verdicts, deadlock that refuses to pick a side, partial answers that do not strand the batch, receipts.
Cases A durable case that can derive its next warranted work. Typed contributions, human-gated capability expansion, policy-derived scoped authority, and one ephemeral worker per contribution.

335 tests, no network, no credentials. Plus three live runs against real infrastructure that found nine bugs the suite did not — each now pinned by a test, each written up in docs/SMOKE-RUN.md rather than quietly fixed.

Where it sits

Complementary, not competing — and the honest version of the comparison is that the durable- execution engines are better at durable execution. What differs is what owns the work.

System What it owns Where abeyance differs
Temporal / Restate Durable execution: replay-based recovery, durable timers, retry policies, task-queue redelivery Their durable thing is code + state, and state alone is inert. Here it is a row any program can read. Restate's Virtual Objects are a genuinely good Store for this — see Concurrency
LangGraph Graph/thread persistence, checkpoints, interrupt() Continuity is a persisted graph intended to resume graph execution. Consent here is detached from whatever asked for it
Agent runtimes (Hermes, OpenClaw, OpenWorker) An always-on gateway: chat channels, models, tools, a browser, and memory persisted as prose They are the better assistant, and this does not replace one — see Why not an always-on agent. Run one as a worker; keep the case and its authority outside it
HumanLayer ACP / AgentGate Approval-gated tool calls; policy routing an action to an approver Not an approval UI or a policy router. This is the durable multi-party decision ledger and the apply loop behind one
JamJet Runtime policy, budgets, replay Use it before; abeyance owns the asynchronous consent process once a human is genuinely required

What is deliberately not provided: replay-based crash recovery mid-execution, durable timers finer than your cron interval, retry policies, exactly-once side effects, serialized single-writer per key. Each of those, and where it lives instead, is tabulated in docs/CASES.md.

Why not an always-on agent

The personal-agent runtimes — Hermes, OpenClaw, OpenWorker — solve a real problem well: one warm gateway, your chat channels, tools, a browser, and memory that persists across sessions. If you want an assistant you can text, use one. This is not a replacement and does not try to be.

Why it cannot be is worth stating precisely, because it is the same reason they cannot become this:

A warm process is an undivided process. Answering in two seconds means the credentials for whatever the request turns out to need are already loaded, in one place, under one identity. That is not a configuration choice — it is what "already warm" means. Per-contribution isolation costs a container boot, which is exactly what an assistant cannot afford. Low latency and divided authority are mutually exclusive in one process. Which one you need depends on whether a mistake is a re-roll or a send.

Prose memory cannot carry authority. Memory in those systems is Markdown, YAML, or a fact store — so "approved" is a word somebody wrote, and the next reader decides its weight. Here a model emitting {"authorized": true, "note": "auto-approving on the owner's behalf"} counted for nothing on every tick, and the refusal was reported as AUTHORITY_CLAIMED. Authority comes from a contribution's kind and the actor's standing. Never from the payload.

A session is not a durable record. Both worker apps deleted, the library moved out until import abeyance failed — and an in-flight case was still advanced by one SQL INSERT, its authority re-derived by 20 lines of SQL that reached the same verdict, including refusing the same forged claim. Transcript.

And a yes in memory does not expire when the facts dothe section above.

A model worker here can be a full agent-runtime invocation. What it cannot be is the thing that holds the authority. examples/model_workers.py is the shipped version: the judgment worker runs the Claude Code CLI headlessly on a subscription credential (CLAUDE_CODE_OAUTH_TOKEN), so the opinion half of a case is flat-rate rather than metered per-token, and metered spend stays confined to the cheap eval-gated extraction rungs. That is a property of running the agent as a bounded worker rather than as the durable centre — a long-lived session burns tokens holding context that a case keeps in Postgres for free.

The mechanism: isolation per contribution, not per process

The capability registry decides which specialist may be requested; the runner, platform app and secrets make the boundary real. In the production shape, a dispatcher starts one ephemeral machine for one contribution. That machine receives the case/request contract plus only the secrets attached to its app, writes one contribution, then is destroyed.

durable case in Postgres
        │
        ├─ registered capability: image + produces + reach + app + timeout
        │
        └─ one ephemeral worker
              ├─ task-specific contract environment
              ├─ only that app's secrets and network reach
              ├─ one typed contribution written to the store
              └─ exits and is destroyed

The worker that reads your campaign database is therefore a different container, in a different app, with a different secret set from the one that writes to your CRM. A model worker can have no production credentials at all. It may recommend; it cannot turn that recommendation into authority.

The reach labels in a Capability are reviewable declarations, not a sandbox by themselves. The real enforcement is one identity and secret set per app or runtime boundary. The shipped FlyMachinesRunner uses Fly's app-level secrets and auto-destroyed machines; LocalProcessRunner is deliberately useful for development but does not provide this isolation. A Docker runner is not required by the library: the Runner protocol is intentionally small so another platform can provide the same boundary.

The corollary is the design's sharpest constraint, and it is a feature:

New behaviour is free. New reach costs a human.

A case can invent any instruction it likes and hand it to a registered worker. What it cannot do is conjure a worker that reaches somewhere no declared capability reaches — that blocks the case and asks a person. Minting a capability can itself run as a case, so the system extends itself through its own approval loop, with no moment where a model grants itself new reach.

Cost, stated plainly: a container boot plus image pull is seconds. This is for work measured in hours to days. It is the wrong tool for a 200ms tool call.

The 60-second version

from abeyance import ApprovalLoop, Item, Approver, ApprovalPolicy
from abeyance.adapters import PostgresStore, GmailTransport

loop = ApprovalLoop(
    "migrations",
    store=PostgresStore(os.environ["DATABASE_URL"]),      # shared, not per-host
    transport=GmailTransport(token_path="~/.gmail/token.json"),
    policy=ApprovalPolicy(threshold="all", veto=True,     # both must say yes
                          expire_after_days=7, max_turns=3),
)

# --- whatever proposes. Runs, sends, exits. ---
loop.propose(
    items=[Item(n=1, summary="Drop legacy_sessions (0 reads in 90d)",
                payload={"table": "legacy_sessions"}),
           Item(n=2, summary="Backfill tenant_id on 4.1M rows",
                payload={"job": "backfill-tenant"})],
    approvers=[Approver("dba@corp.com", role="dba", channel_id="U123"),
               Approver("lead@corp.com", role="lead", channel_id="U456")],
    subject_key="prod-migration-114",
)

Someone replies, in prose, from their phone:

approve 1, hold 2 until after the release

# --- the apply worker. A cron entry. Knows nothing but the store. ---
if poll := loop.poll():                    # deterministic. no model. no tokens.
    for pid in poll.actionable:
        for inbound in loop.read(pid):     # parses; records NOTHING
            loop.record_from(pid, inbound) # refuses anything hedged; escalates instead
        loop.execute(pid, executor=run_migration)

run_migration is called for item 1 and never for item 2 — and never at all until both approvers have answered.

Run it now, no credentials needed:

git clone https://github.com/kkrlstrm/abeyance && cd abeyance
pip install -e ".[dev]" && python -m pytest -q

python examples/01_single_approver.py              # the whole library in 40 lines
python examples/02_two_approvers_and_a_deadlock.py # two people, one disagreement
python examples/03_scheduled_worker.py             # the production cron shape
python examples/04_local_case.py                   # a whole CASE — no Postgres, no containers

04_local_case.py is the one to run if you want to see the case layer work before deciding whether to stand any infrastructure up. It uses JSONFileStore and LocalProcessRunner, so the only dependency is abeyance itself, and it walks the whole arc: a case opens needing evidence, a worker subprocess writes it, a rule derives a second need nobody planned, a worker returns a payload claiming approval and is refused with AUTHORITY_CLAIMED, a decider with standing grants authority, and execute() acts once under a scope narrowed to what every contributor allowed. Two things it deliberately does not fake: LocalProcessRunner is not isolated, and JSONFileStore has no atomic claims — both are stated in the file rather than glossed.

What it actually gets right

A reply parser is a suggestion, never authority

interpret("approve 1 but can you reword the second line first", n_max=3)
# → Suggestion(approve=[1], mode="explicit", conditional=True, confident=False)

That reply is a request for another draft. Reading it as a yes ships text nobody agreed to. interpret() handles "1 and 3", "all except 2", "1-4, skip 3", "none of these" — and marks conditionals, bare numbers, and lone affirmations not confident. read() returns suggestions and records nothing; record() takes explicit item numbers. Something with judgment sits between them, and record_from(..., require_confident=True) escalates rather than guessing.

Every item gets a real verdict

Not a boolean. Five outcomes, and the distinctions carry decisions:

Verdict Meaning What you do about it
APPROVED cleared the threshold run it
REJECTED someone decided against it do not run it, do not re-ask
DEADLOCKED people with equal standing disagreed write nothing, escalate to a human
UNREACHABLE nobody vetoed it; it can no longer reach the threshold worth re-proposing
WAITING not enough information yet ask again later

DEADLOCKED is the one most systems collapse. Taking the majority view, or the most recent reply, resolves a genuine human disagreement by accident of implementation. Here the item is not written, the proposal ends DEADLOCKED, and an escalation goes to an owner who is deliberately not an approver — off the consent path, on the exception path.

A partial answer does not strand the rest of the batch

A five-item digest comes back with "approve 1 and 3". Under unanimity the other three can never pass — but "she answered and passed over them" is not the same fact as "she has not read it yet", and a system that reports both as waiting hangs until expiry with real decisions dying inside it. UNREACHABLE names the difference so the remainder can be re-proposed the same day.

Consent happens in the channels people already use, and can span days

Email is the reference transport because a reply comes back from a phone, on a plane, at 11pm, without a login. Approvers are identified by the sender of their reply, tracked independently, and can answer days apart. Nudges follow a schedule and stop at a cap, because an uncapped reminder is how a useful loop becomes a filter rule.

Expiry runs off the last recorded activity, so a conversation in progress does not time out mid-sentence — see the exact rule below, which is narrower than "any reply resets it".

Every approval ends with a receipt

acme — here is what changed, and why.

  • Add Directors of Curriculum & Instruction to the target personas
      file: personas/district-leader.md

Left alone — you disagreed on item 3. Nothing was written for it and a
human has been asked to break the tie.

Sent as a fresh thread, optionally to a wider audience than the approvers. This is the leg most approval systems skip, and its absence is why people stop trusting one.

No silently skipped work

For loops that scan a source and propose on what is new, the watermark is guarded. A cursor that advances after a failed send skips that window permanently and silently — no error, no retry, nothing in a log, because from the system's point of view it was handled.

with gate.begin("acme") as run:
    run.require("ledger", "the append-only record of what we read")
    run.require("digest", "the approval request itself")

    write_ledger(findings);          run.satisfied("ledger", ref=sha)
    result = loop.propose(...);      run.satisfied("digest", ref=result.id)

    run.advance(marks={"last_event": ts})   # raises if either is outstanding

The rule becomes an exception you cannot miss instead of a comment somebody has to remember. DueGate (which subjects are due, plus a floor sweep so a dead trigger cannot masquerade as a quiet week) is optional and documented in docs/ARCHITECTURE.md.

What this does not claim

The value here is in being exact, so:

Claims prevent overlapping live workers, not exactly-once side effects. record() is idempotent per approver and execute() refuses an already-executed proposal. A claim-capable shared store (PostgresStore in production) can atomically claim an approval execution with execute_claimed(), and can claim a whole case tick with claimed(). Those claims close the live-worker race; they cannot tell whether an external side effect happened just before a worker crashed. Keep external executors idempotent on the request id.

Sender attribution is not cryptographic identity. An approver is identified by the From address on their reply. That is an operational control, not authentication — no DKIM check, no signature verification. Adequate for an internal mailbox; not adequate on its own where spoofing is in your threat model. Put a verified channel in front of it if it is.

Expiry restarts on recorded activity, not on any inbound. The clock moves when a decision is record()ed, a reply is dismiss()ed, you ask() a clarification, or the proposal is executed. Merely fetching an inbound reply with read() or poll() does not move it. That is deliberate — otherwise an out-of-office auto-reply keeps a dead proposal alive forever — but it means an ambiguous reply arriving near the deadline needs a deliberate act to extend it. record_from() raises an AMBIGUOUS_REPLY escalation in exactly that case, so it is surfaced rather than silent.

Not a policy engine, a scheduler, or an agent framework. If you want "auto-approve anything under $50", decide that before you propose. Bring your own cron. This library's job starts once a human is genuinely required and ends when the receipt is sent.

Adapters

The core is a state machine and a parser with no dependencies. Everything touching the world sits behind one of four protocols.

Seam Ships with Extra
Store MemoryStore, JSONFileStore, PostgresStore [postgres]
Transport MemoryTransport, GmailTransport, SMTPIMAPTransport, CallableTransport [gmail], else stdlib
Notifier SlackNotifier, WebhookNotifier, Console, Recording, Null stdlib
Clock SystemClock, FrozenClock

Use a shared store the moment a second host exists. Per-host state is not merely inconvenient, it is silently wrong: each host reads its own cursors, so a machine that stopped running the loop keeps a frozen snapshot it cannot distinguish from "nothing happened".

Already have a mail helper? CallableTransport wraps it — the migration is a one-file change, not a credentials project.

CLI

Every subcommand prints JSON; exit codes are categorical so a shell runner can branch without parsing prose (0 fine · 2 usage · 3 not found · 4 blocked · 5 transport).

abeyance --app myapp:loop poll                 # the free gate — exit early when quiet
abeyance --app myapp:loop read   --id T
abeyance --app myapp:loop record --id T --from a@b.com --approve 1,3
abeyance --app myapp:loop apply  --id T --executor myapp:run
abeyance --app myapp:loop nudge --dry-run
abeyance --app myapp:loop inject --id T --from a@b.com --text "approve 1"   # rehearsal

poll() is deterministic by construction — state read, transport read, ids returned. No model call, no interpretation, no writes. An hourly tick over a hundred open proposals costs a hundred API reads and zero tokens, which is why a shell runner should call poll first and exit when it prints an empty actionable list.

inject drives the whole state machine with no mailbox and no humans — the multi-approver paths are the ones worth rehearsing before real people are on the thread.

Cases — when the work needs more than one kind of contributor

The approval layer detaches consent from the runtime that asked for it. The case layer applies the same move to work needing a machine to gather facts, a model to form an opinion, and a person to decide — each arriving from its own process, with nothing alive in between.

from abeyance import Capability, CapabilityRegistry, CaseLoop, Need, when_payload
from abeyance.adapters import FlyMachinesRunner, PostgresStore

registry = CapabilityRegistry([
    Capability(name="bison-evidence", image="postgres:16-alpine",
               produces=("campaign-performance",), reach=("db-read",),
               app="workers-data", timeout_seconds=120),
])

cases = CaseLoop("launches", store=PostgresStore(DSN), registry=registry,
                 runner=FlyMachinesRunner(app="workers-data"), approval=approval_loop,
                 rules=[when_payload("deliverability-check", given="campaign-performance",
                                     key="gone_quiet", carry=("client",))])

case = cases.open(action="launch-campaign", subject_key="acme",
                  needs=[Need("campaign-performance", spec={"client": "Acme"})])

# ... an hourly cron line, on any host, in any process ...
for report in cases.tick(harvest_standing={"owner@acme.com": ("launch-campaign",)}):
    if report.actionable:
        cases.execute(report.case_id, executor=launch)

Three properties, and each is the reason for a file:

Authority comes from the contribution's type and the actor's standing — never from the payload. A model emitting {"verdict": "approved", "authorized": true} has said something with exactly zero authority, and the refusal is reported rather than silent. EVIDENCE and RECOMMENDATION can never authorize; only a DECISION from an actor whose standing covers the action can. (standing.py)

The case derives the path; new reach costs a human. A rule can add the next need when the evidence warrants it, and that worker can receive a new spec without new infrastructure. A need no registered capability can satisfy blocks the case and asks a person. Minting a capability can itself be run as a case, so the system extends itself through its own approval loop, with no moment where a model grants itself new reach. (warrant.py, capability.py)

A dispatch that vanishes is detected. You ask for a container, the platform accepts, and it never boots. Nothing throws, and the request looks exactly like work in progress. A lease plus an attempt count is what turns that into a retry and then a loud failure. (dispatch.py)

One ephemeral worker per contribution is the isolation model: the worker that reads the campaign database is a different container, in a different app, with a different secret set, from the one that writes to your CRM. The platform's app identity and secrets enforce that boundary; Abeyance records which registered boundary the case may request. The cost is a container boot — this is for work measured in hours to days, not a 200ms tool call.

See docs/CASES.md for the full picture, including what is deliberately not provided (replay-based recovery, retry policies, exactly-once) and where those live instead.

Docs

  • docs/ARCHITECTURE.md — the state machine, the verdict rules, the seams, the concurrency boundary
  • docs/CASES.md — the case layer: typed contributions, standing, reach, dispatch leases, scoped authority
  • docs/SMOKE-RUN.md — a real run against Fly machines, live Postgres and a Gmail thread, including the two bugs it found that the test suite did not
  • docs/FAILURE-MODES.md — twelve silent failures the design is shaped around, each pinned to its test

Licence

Apache-2.0 — see LICENSE and NOTICE.

Permissive on purpose. This is meant to sit inside other people's control planes, and a copyleft licence is a procurement conversation rather than a pip install — several of the organisations most likely to need durable consent are the ones with a blanket ban on it. Embed it, modify it, ship it in a closed product; the patent grant is included and attribution is the only ask.

Sibling repos in the same line of work: agent-guard, agent-tenancy, cc-logger, wroteonly.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

abeyance-0.2.1.tar.gz (194.1 kB view details)

Uploaded Source

Built Distribution

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

abeyance-0.2.1-py3-none-any.whl (135.5 kB view details)

Uploaded Python 3

File details

Details for the file abeyance-0.2.1.tar.gz.

File metadata

  • Download URL: abeyance-0.2.1.tar.gz
  • Upload date:
  • Size: 194.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for abeyance-0.2.1.tar.gz
Algorithm Hash digest
SHA256 013d32dd4a5c7c1726f0c4eb4e7adcd4d35fee63f5b0ff0dd6f5683153bbf37a
MD5 c2fc7598a91513b6dd563ef17813f9a9
BLAKE2b-256 daca4d91d37762ea8769e2c6839fb829cc15521e59b598aab58eff4c146c7477

See more details on using hashes here.

File details

Details for the file abeyance-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: abeyance-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 135.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for abeyance-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e4a4b910cbb5e703808cfa014539b9715cadc901f7a6839ef5d1c3fcd1c5ed61
MD5 67dcbb8d639e9a2ff86bd9c505a09715
BLAKE2b-256 b7995532ce97814a4ffd5b4d06d56d74afba076d13914fd75047f0731cc3d506

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page