Skip to main content

aflib

Helper library for business logic inside Airflows plpython3u function bodies.

An Airflows function body is a bare Python snippet with a plpy global. It cannot import another function body, cannot see the HTTP request, and cannot shape the HTTP response — so every body ends up re-implementing the same parameter parsing, the same row reading and the same result envelope. aflib is that plumbing, written once.

Status: early, and deliberately so. Nothing enters this library until it has run on a real Airflows instance — written first as a plain function body, deployed, called, and observed. One version corresponds to one proven capability, which is why the numbers start so low.

Install

Airflows resolves Python packages from public PyPI by name and version only; there is no local-import, git or private-index route. Declare it in models/pythonPackages/pythonPackages.airflows:

pythonPackage aflib==0.0.2

Then import it from a function body.

What 0.0.2 gives you

from aflib import Db, errors, fail, logging, ok, params

db = Db(plpy)
log = logging.logger(plpy, source="Economic.receipt_cancel")

try:
    receipt_id = params.as_int(p_id_receipt, name="p_id_receipt", required=True)

    row = db.query(
        'SELECT id, amount FROM "Economic"."Receipt" WHERE id = $1',
        [receipt_id], ["integer"],
    ).one()

    if row is None:
        return fail("not_found", "No receipt with that id")

    return ok(row)
except Exception as exc:
    return errors.to_fail(exc, log=log)

The try is not optional, and except Exception is not laziness. See Never let an exception escape below — both are load-bearing.

  • Db(plpy) — plpy is injected, never imported, because the function body is the only composition root available. db.query(...) returns a Rows you narrow with .one() (None for no row, and it refuses more than one rather than silently taking the first), .rows() for all of them, or .scalar(column) for a single named value.
  • params.as_int / as_bool / as_decimal / as_date / as_json — every HTTP parameter arrives as text and must be declared text, so every body converts. These raise BadParameter, which is the caller's fault and should become a 4xx-shaped failure rather than an uncaught exception.
  • ok(data) / fail(code, message) — a body's only output is its return value, which the platform wraps as [{"result": …}]. There is no status code and no header, so success and failure both have to be expressed inside the value.
  • codec.dumps / loads — JSON that raises on anything it cannot serialize instead of stringifying it. numeric columns arrive as decimal.Decimal and are encoded as strings, never floats, so exactness survives the round trip.
  • errors.to_fail(exc, log=log) — converts any exception into a failure envelope, keeping the caller's message and the log's detail as two separate strings. A BadParameter is described to the caller in full, because it is about their own input; anything else becomes a generic internal_error whose detail goes only to the log. It returns the envelope even if logging itself fails, because a broken log sink must not turn a handled failure back into an escaping one.
  • logging.logger(plpy, source=…) — one JSON object per line, level and event always present, written through plpy.log. It never raises, even when handed a value that cannot be encoded: the line is most needed in the error path, so a single exotic field must not be able to silence it.

Platform constraints worth knowing before you build on it

These are observed on a live instance, not inferred from documentation. They are the reason the library has the shape it does.

  • Every HTTP-facing parameter must be declared text. Declare integer and the platform binds varchar at call time, no candidate signature matches, and the endpoint fails with function does not exist — it cannot be invoked at all. This is not a casting inconvenience; the endpoint is simply unreachable. Convert inside the body instead.
  • Function bodies compose only through SQL — SELECT "Schema"."fn"(…). There is no shared module and no package-local import, so a helper another body needs is a database function, not a Python one.
  • A body cannot see the request or shape the response. Its globals are exactly GD, SD, args, its declared parameters and plpy: no request object, no undeclared query parameters, no body, no headers. It cannot set a status code, a header or a redirect.
  • jsonb is never decoded — it always arrives as str, even through plpy.prepare. numeric arrives as decimal.Decimal and bytea as bytes; neither is JSON-serializable by default, and numeric is every money column.
  • Never cache request-scoped state in GD or SD. Consecutive calls land on different pooled backends, and the connection role varies per caller.

Never let an exception escape

An unhandled exception's message is returned to the caller, and it includes internal detail that the caller should not see. A function body cannot influence that response: it is built outside the database from the driver's error message, and there is no setting that trims it. The only way to control what a caller receives is to not raise — catch everything and return a failure envelope instead. That is what errors.to_fail is for, and it is why every example here wraps its work in a try.

except Exception is required rather than merely convenient. plpy.Error does not inherit from plpy.SPIError, so the narrower except plpy.SPIError catches neither plpy.error() nor an ordinary Python exception — only their common ancestor covers all three.

Two related facts worth knowing before you design around logging:

  • Of plpython's five informational sinks, only log and warning are written. debug, info and notice return successfully and are discarded, because log_min_messages defaults to WARNING and those three rank below it while log ranks above. A level that vanishes is worse than one that does not exist, which is why aflib.logging puts severity in the payload and writes everything through plpy.log.

  • The platform logs every call's parameter values in plaintext before the body runs. So never pass a secret as a function parameter — it is recorded no matter how careful the body is — and do not log parameter values yourself, since that cannot reduce the exposure and only makes it look deliberate.

  • A callee's error message reaches its caller unredacted. Function bodies compose through SQL, and when a callee raises, its message crosses to the caller intact while its exception type does not. So a business outcome has to travel as a returned value — ok/fail — and never as an exception, and nothing sensitive may ever go into an exception message.

Tests

uv run pytest

The unit tests run against a fake plpy, which verifies control flow and nothing transactional. Anything involving a savepoint is proven on the platform, not here.

License

MIT — see LICENSE.

Metadata

Release files for aflib 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aflib 0.0.2
File Size Uploaded Provenance
aflib-0.0.2.tar.gz 27.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for aflib 0.0.2
File Interpreter ABI Platform
aflib-0.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 44.4 kB

Release files / aflib-0.0.2.tar.gz

Download URL aflib-0.0.2.tar.gz
Size 27.1 kB
Tags Source
SHA-256 checksum
How to use checksums
75a49730d894c8abff58cdb479f494c1d911a6176399ed3c8d97b54f14fdac33
BLAKE2b-256 checksum
How to use checksums
610d86535df831808d29a53a7f22e0d879a2c66beda3c619294c99013ec4fdfb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / aflib-0.0.2-py3-none-any.whl

Download URL aflib-0.0.2-py3-none-any.whl
Size 17.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7199e4dd5c0762ff04ff59889bf689b484c1199d6b8366ebfd7346ae74dde1e8
BLAKE2b-256 checksum
How to use checksums
783bc478c5fbe2e21b9886e859593e8014e8d3970371a85eb39603257be3ed4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for aflib 0.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page