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)—plpyis injected, never imported, because the function body is the only composition root available.db.query(...)returns aRowsyou narrow with.one()(Nonefor 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 declaredtext, so every body converts. These raiseBadParameter, 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.numericcolumns arrive asdecimal.Decimaland 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. ABadParameteris described to the caller in full, because it is about their own input; anything else becomes a genericinternal_errorwhose 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,levelandeventalways present, written throughplpy.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. Declareintegerand the platform bindsvarcharat 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 andplpy: no request object, no undeclared query parameters, no body, no headers. It cannot set a status code, a header or a redirect. jsonbis never decoded — it always arrives asstr, even throughplpy.prepare.numericarrives asdecimal.Decimalandbyteaasbytes; neither is JSON-serializable by default, andnumericis every money column.- Never cache request-scoped state in
GDorSD. 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
logandwarningare written.debug,infoandnoticereturn successfully and are discarded, becauselog_min_messagesdefaults toWARNINGand those three rank below it whilelogranks above. A level that vanishes is worse than one that does not exist, which is whyaflib.loggingputs severity in the payload and writes everything throughplpy.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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| aflib-0.0.2.tar.gz | 27.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.