Changelog
All notable changes to this project are documented here. The format follows Keep a Changelog and this project adheres to Semantic Versioning.
Semver covers the names exported from databaas_utils.__all__ and
databaas_utils.airflow.__all__, the Airflow connection field contract, and the
Iceberg index schema. Anything underscore-prefixed or absent from __all__ is
not covered.
[0.6.0] - Unreleased
Added
databaas dag deploydeploys a repo's Airflow code from CI. It readsbundle.name,targetsandartifacts.dagsfrom the repo'sdatabaas.ymland syncs that directory todags/prod/<bundle>/ordags/dev/<dev-key>/<bundle>/in the environment's DAG bucket, the layout the platform'sDataBaasS3DagBundlereads. Files that changed are uploaded and files that were removed are deleted, never outside the target's prefix, and a_databaas_target.jsonis written fortarget_forandscoped().--dry-runlists the changes without making them.- No login needed. The command authenticates only with the environment's DAG
deployment key in
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY. It reads the endpoint fromAWS_ENDPOINT_URLand the bucket fromDATABAAS_DAGS_BUCKET, or from--endpoint-urland--bucket. Without the key it stops and says so. That is expected on a laptop: deploying with your own login, through Lakekeeper, comes later. - Development targets are named after the branch (
GITHUB_HEAD_REF,BITBUCKET_BRANCH, else git), as the same slug the per-repo workflows computed, and--dev-keyoverrides it. Before upload, a dev deploy renames the repo's top-level packages and modules to<name>_<dev_key>and rewrites their imports, so two open branches cannot shadow each other on Airflow's sharedsys.path. This is done on a staged copy; the checkout is not changed. databaas dag validateparses a target's DAGs with Airflow'sDagBagbefore a deploy, staged exactly asdeploywould upload them and with the bundle appended tosys.pathas in the cluster. It fails on import errors, on no DAGs, and on a dev target's dag id without thedev_<dev-key>_prefix, reports problems against the repo's own files, and adds GitHub annotations in Actions. It needs Airflow and the DAGs' dependencies installed, and no key. It replaces the per-repo validate scripts and their copy of thedatabaas_targetshim.databaas dag destroydeletes a target's prefix, for cleanup when a pull request closes. Production needs--force.deployextra (PyYAML, boto3);allincludes it.
[0.5.3] - 2026-10-08
Fixed
- Using a second Iceberg table no longer breaks the first with
403 Forbidden. pyiceberg'sFsspecFileIOgives every table the same S3 client and re-points its signer at whichever table loaded last, so table A's reads and writes were signed for table B and refused. Its per-thread filesystem cache also re-registers the signer on that shared client from each worker thread, and a request in flight at that moment went out unsigned; that madeoverwriteand other multi-threaded operations fail intermittently.rest_catalognow loads aLakekeeperCatalogwhose tables use aSigningFileIOwith S3 clients of their own, signer attached before first use. Lakekeeper'sloadTableresponse names the stockFsspecFileIO, which the catalog swaps forSigningFileIO.
Changed
- The integration stack pulls MinIO from
cgr.dev/chainguard/minio.minio/minioandminio/mcnow refuse anonymous pulls on both Docker Hub and quay.io, so CI could not start the stack.
[0.5.2] - 2026-10-08
Added
databaas profile delete NAMElogs the profile out, removes its keychain entry and drops it fromconfig.toml. Deleting the default profile leaves no default; the command names the remaining profiles to pick from withdatabaas profile use. On macOS it also removes the profile's pre-0.5.2databaas-utilsentry when it can, and says so when only the Python installation that saved it may.
Fixed
- Logging in again on macOS no longer fails with
-25244once Python has changed. macOS lets only the binary that created a Keychain entry replace it, and throughkeyringthat binary was the Python interpreter, so a uv Python upgrade, another virtualenv or another Python version could not save the rotated refresh token. On macOS the refresh token is now written through/usr/bin/security, whose identity does not change, and the access token is cached in a 0600 file under<cache_dir>/sessions.DATABAAS_TOKEN_STORE=keyringon macOS selects this store too. databaas logoutno longer crashes when the issuer cannot be reached. Revoking the refresh token was meant to be best effort, but fetching the issuer's OIDC metadata first could raise, so logging out of a removed environment or while offline failed.
Changed
- Log in once more on macOS after upgrading. The new store uses its own Keychain
service,
databaas-refresh-token. Entries under the olddatabaas-utilsservice are no longer read; delete them in Keychain Access if you want them gone.
[0.5.1] - 2026-10-06
Fixed
- A login saved by another Python installation no longer crashes with a keyring
traceback. On macOS only the program that created a Keychain entry may replace or
remove it, so a second virtualenv logging in to the same profile hit
PasswordSetError (-25244). Saving now raises aConfigurationErrornaming the profile and the ways out:databaas logout --profile NAMEfrom the other installation, deleting the entry in Keychain Access, another--profile, orDATABAAS_TOKEN_STORE=file. databaas logoutno longer reports success when the Keychain refuses to remove the entry. A refusal for an entry that still exists is now the same clear error; a missing entry is still a no-op.
[0.5.0] - 2026-10-06
Added
- Package documentation. A
docs/site (mkdocs) with guides for notebooks, Airflow DAGs and laptop login, and a generated API reference. Setting up the Airflow connection is documented at https://docs.databaas.eu/pipelines/service-account; the Zitadel app for local login is provisioned by the platform. Build it withuv run --group docs --all-extras mkdocs build. - Docstrings on the whole public API, with arguments, raised errors and
examples, so editor hover and
help()are useful. Ruff's pydocstyle rules (Google convention) now fail CI on an undocumented public name.
Changed
databaas login URLderives the Zitadel and catalog addresses from the portal's base URL (auth.<host>,catalog.<host>) and defaults the warehouse todatabaas-lakehouse. The client id comes from the portal's/.well-known/databaas-client.json, which now only needsclient_id;--client-idskips that lookup, and--issuer,--catalog-urland--warehousestill override.--project-idis optional. Without it,databaas loginrequests nourn:zitadel:iam:org:project:id:<id>:audscope: Zitadel already puts the app's own project in the token's audience, which is what Lakekeeper and Trino check. A saved profile may have an emptyproject_id.- The warehouse defaults to
databaas-lakehouseeverywhere it is read from configuration:databaas login(all forms), saved profiles,LakehouseSettings.from_mapping()andfrom_env()(LAKEKEEPER_WAREHOUSEis now optional), and the Airflow connection's Warehouse field. Set it only to use another warehouse. TheLakehouseSettingsconstructor still takes it explicitly. databaas loginanddatabaas whoamishow your email again. Zitadel leaves it out of the access token, so it is now read from the issuer's userinfo endpoint (UserLogin.userinfo()); the user id is shown when that is unavailable.databaas_utils.notebookis nowdatabaas_utils.user. Its helpers authenticate as the person running the code, on a laptop as much as in a notebook, and the old name hid that.databaas_utils.notebookstays as an alias, so existing imports keep working.
[0.4.0] - Unreleased
Added
- Notebook access as the logged-in JupyterHub user.
JupyterHubTokenis aTokenProviderthat fetches the user's access token from the Databaas hub'sGET /hub/api/oauth-token, authenticating with theJUPYTERHUB_API_TOKENevery singleuser pod carries. It caches the token until shortly before its ownexp— each hub call rotates the refresh token at Zitadel — and hands pyiceberg and the S3 signer a live reference, so a notebook keeps working past the one-hour token lifetime without re-creating its catalog.LakehouseSettings.from_jupyterhub()builds settings from the pod's environment (CATALOG_URI,LAKEKEEPER_WAREHOUSE); region and endpoint come from Lakekeeper's table config. Replaces thedatabaas_authhelper previously copied into the notebooks image. databaas_utils.notebook—get_catalog(),get_store(),get_index(),get_zarr_group()andget_settings()for notebooks, mirroringdatabaas_utils.airflow's helpers. Inside JupyterHub they use the hub token; anywhere else thedatabaas loginsession.profile=forces a login profile.
[0.3.0] - Unreleased
Added
- Local, interactive login as your own Zitadel identity — no service
account needed.
databaas login(a newdatabaasconsole script) opens a browser for Authorization Code + PKCE, or--devicefor the OAuth2 device-code flow on machines without a reachable browser. The session is cached across Python processes: the access token in memory, the refresh token in the OS keychain via a newloginextra (keyring,filelock).databaas whoami/token/logoutmanage it;databaas profile list/useswitch between named environments saved in~/.config/databaas/config.toml. LakehouseSettings.from_profile()— the Python-side entry point, building settings from a saved profile and aUserLogincredential. NewProfile,load_profile,UserLoginandlogin()exports.- New
localextra (databaas-utils[local]) for the analyst install:iceberg,obstore,zarrandloginin one. databaas loginalso registers the caller with Lakekeeper (POST /management/v1/user), since the platform's own pre-provisioning webhook is currently broken — see the README's admin runbook for the Zitadel app this needs.
Fixed
- A custom
TokenProvider(including the newUserLoginand the existingServiceAccountKey) is no longer frozen into a single static bearer when building a pyiceberg catalog or a pylakekeeper client. Both now hold a live reference to the provider and callauth_header()fresh on every request, so a session whose access token is short-lived (an hour, for a user login) keeps working past that hour instead of failing once the frozen token expired.
0.2.1 - 2026-09-18
Fixed
__version__now actually matches the installed version. It was a hand-maintained literal, separate frompyproject.toml's version — bumped there for 0.2.0, never bumped here, so the published 0.2.0 wheel reported__version__ == "0.1.0". Derived from installed package metadata instead, so the two cannot drift apart again.
0.2.0 - 2026-09-18
Added
ServiceAccountKey— Private Key JWT (RFC 7523) authentication for Zitadel service accounts, for consoles that only offer a downloadable JSON key for a machine user, not a plain client secret. Signs a JWT assertion with the key's private key and exchanges it for a real access token via theurn:ietf:params:oauth:grant-type:jwt-bearergrant; the resulting token is a normal Zitadel-issued JWT, indistinguishable to Lakekeeper from one obtained viaclient_credentials.ServiceAccountKey.from_json()builds one directly from the key file's own contents, its raw JSON text, or a path to it. Takes priority overclient_id/tokenwhen a connection'sservice_account_keyextra is set.- New
[jwt]extra (PyJWT[crypto]), pulled into[airflow]and[all]. Kept out of core dependencies: it pulls incryptography, a real compiled dependency most callers — anyone using a client secret or a static token — never need.
Fixed
- A Zitadel Personal Access Token does not work against Lakekeeper, and
is not a supported credential form here. A PAT is an opaque credential
meant for calling Zitadel's own APIs, not a JWT a resource server's OIDC
validator can parse — Lakekeeper rejects it with a plain
AuthenticationFailed, before any permission check.ServiceAccountKey(above) is the fix for the console configurations that made a PAT look like the only available option.
0.1.0 - 2026-09-16
First release. Extracted from Infoplaza's lakehouse_core, which shipped as a
directory synced into a DAG bundle rather than as a package.
Added
LakehouseSettings, with credentials as a sum type (OAuthClientCredentials/StaticToken/ anyTokenProvider) rather than six nullable fields validated against each other.CatalogClientandrest_catalog, wired for Lakekeeper's remote signer.GenericTableStore— blob access under generic tables, signed rather than credentialed, withput/get/open/list/delete.FileIndex— an Iceberg record of what landed in the generic tables.open_zarr_group— zarr groups backed by the signed object store.scoped/slugfor deployment-scoped naming, and the Airflow layer:LakehouseHook(a registereddatabaas_lakehouseconnection type with a working Test button),get_catalog/get_store/get_index/get_zarr_group, and thedatabaas_targetdeployment shim.
Fixed
- Remote signing is now actually enabled. The predecessor set
s3.remote-signing-enabled, which pyiceberg does not implement, and leftpy-io-implunset — so the catalog resolved toPyArrowFileIO, which cannot sign, and reached S3 with whatever ambient credentials the process carried.rest_catalognow setspy-io-implands3.signer, and leavess3.signer.urito the server'sloadTableresponse. - Plaintext endpoints work. obstore's Rust
object_storerejects anhttp://endpoint withBadSchemebefore any I/O unlessallow_httpis set;signed_storenow sets it when, and only when, the endpoint is already plaintext.
Changed
- An unresolvable connection now raises
ConnectionNotConfigured. The predecessor returnedNone, which callers turned into a local filesystem sink — so a typo'dconn_idredirected a production write to an ephemeral pod disk and the task went green. Local storage is now an explicitlocal_root=. FileRecordno longer requiresproductorforecast_hour, and gained anattributesmap. The predecessor's schema encoded weather-forecast knowledge a general-purpose index should not demand.