Settings and credentials
databaas_utils.settings
How to reach a Lakekeeper warehouse, and how to prove who you are.
Credentials are a sum type rather than a bag of nullable fields: the prior art
carried six optional attributes and validated their combinations in
__post_init__, which made several invalid states representable. Here an
OAuthClientCredentials or a StaticToken (or any TokenProvider) is
supplied, and there is nothing left to validate.
LakehouseSettings is frozen so it can safely key a cache. Namespace is the
one field that varies per call site, so it is changed with with_namespace
rather than by mutation.
LakehouseSettings
dataclass
LakehouseSettings(
base_url: str,
warehouse: str,
credentials: Credentials,
namespace: str = "default",
region: str = DEFAULT_REGION,
endpoint: str | None = None,
verify: bool | str = True,
)
Everything needed to reach one warehouse as one identity.
Frozen, so it is safe as a cache key. Most code never builds one directly:
use :meth:from_env, :meth:from_profile, :meth:from_jupyterhub, or (in
a DAG) databaas_utils.airflow.get_settings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str
|
The Lakekeeper server root, e.g.
|
required |
warehouse
|
str
|
Warehouse name. |
required |
credentials
|
Credentials
|
Any :class: |
required |
namespace
|
str
|
Default namespace for tables and generic tables. |
'default'
|
region
|
str
|
Storage region, used until Lakekeeper's table config overrides it. |
DEFAULT_REGION
|
endpoint
|
str | None
|
S3 endpoint override, for MinIO and similar. |
None
|
verify
|
bool | str
|
TLS verification: |
True
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If |
Example
settings = LakehouseSettings( ... base_url="https://lakekeeper.example.com", ... warehouse="databaas-lakehouse", ... credentials=StaticToken("eyJ..."), ... namespace="open_meteo", ... )
catalog_url
property
catalog_url: str
The Iceberg REST endpoint: base_url plus /catalog.
auth_header
auth_header() -> str
The current Authorization header value, from credentials.
with_namespace
with_namespace(namespace: str) -> LakehouseSettings
A copy pointing at namespace; self if it already does.
from_mapping
classmethod
from_mapping(
values: Mapping[str, Any],
*,
namespace: str | None = None,
) -> LakehouseSettings
Build from a flat mapping. Shared by the env and Airflow adapters.
Credentials are chosen by which keys are present, in this order:
service_account_key, then client_id (+ client_secret), then
token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
Mapping[str, Any]
|
Keys |
required |
namespace
|
str | None
|
Overrides |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If the mapping has no credentials, or a credential set without its token endpoint. |
from_env
classmethod
from_env(
prefix: str = "LAKEKEEPER_",
*,
env: Mapping[str, str] | None = None,
namespace: str | None = None,
) -> LakehouseSettings
Build from the environment a container or notebook already carries.
Reads {prefix}URI, WAREHOUSE, CLIENT_ID, CLIENT_SECRET,
OAUTH2_SERVER_URI, PROJECT_ID, OAUTH2_SCOPE, TOKEN,
REGION and ENDPOINT.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
Variable-name prefix. |
'LAKEKEEPER_'
|
env
|
Mapping[str, str] | None
|
Mapping to read instead of |
None
|
namespace
|
str | None
|
Default namespace. |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If the variables do not form a complete config. |
from_profile
classmethod
from_profile(
name: str | None = None,
*,
namespace: str | None = None,
interactive: bool = True,
store: Any = None,
) -> LakehouseSettings
Build from a saved local profile, authenticating as the logged-in user.
This is the entry point for local, interactive use — log in first with
databaas login (see databaas_utils.cli). Machine contexts
(Airflow, CI) should keep using from_env/from_mapping with a
service credential; a browser login is not something a scheduled task
can complete.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Profile name; the default profile if omitted. |
None
|
namespace
|
str | None
|
Default namespace. |
None
|
interactive
|
bool
|
Whether to open a browser/device login when no valid
session is cached. Set |
True
|
store
|
Any
|
A |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If no such profile exists. |
Example
settings = LakehouseSettings.from_profile() catalog = rest_catalog(settings)
from_jupyterhub
classmethod
from_jupyterhub(
*,
namespace: str | None = None,
warehouse: str | None = None,
env: Mapping[str, str] | None = None,
) -> LakehouseSettings
Build inside a JupyterHub singleuser pod, as the logged-in user.
The token comes from the hub (see databaas_utils.jupyterhub). The
pod's environment names the catalog: CATALOG_URI (the Lakekeeper
server root) and LAKEKEEPER_WAREHOUSE. The storage region and
endpoint are not read here — Lakekeeper supplies both in each table's
config, which takes precedence over the client's.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
str | None
|
Default namespace. |
None
|
warehouse
|
str | None
|
Warehouse name; |
None
|
env
|
Mapping[str, str] | None
|
Mapping to read instead of |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If this is not a JupyterHub singleuser server. |
TokenProvider
Bases: Protocol
Anything that can produce an Authorization header value.
This is the seam that lets a notebook pass the logged-in user's bearer instead of a service account, without this package learning what JupyterHub is.
auth_header
auth_header() -> str
The full Authorization header value, e.g. "Bearer eyJ...".
Called on every request, so implementations should cache and refresh.
StaticToken
dataclass
StaticToken(token: str)
A bearer token supplied verbatim. Never refreshed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The raw access token (a JWT that Lakekeeper can validate). |
required |
Example
settings = LakehouseSettings( ... base_url="https://lakekeeper.example.com", ... warehouse="databaas-lakehouse", ... credentials=StaticToken("eyJ..."), ... )
auth_header
auth_header() -> str
Bearer <token>.
OAuthClientCredentials
dataclass
OAuthClientCredentials(
client_id: str,
client_secret: str,
token_url: str,
scope: str | None = None,
project_id: str | None = None,
_cache: dict[str, Any] = dict(),
_lock: Lock = threading.Lock(),
)
OAuth2 client-credentials, cached and refreshed on demand.
project_id is Zitadel shorthand: it expands to the audience scope that
Lakekeeper expects. Supply scope directly to override.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client_id
|
str
|
The machine user's OAuth client id. |
required |
client_secret
|
str
|
The matching client secret. |
required |
token_url
|
str
|
The Zitadel token endpoint, e.g.
|
required |
scope
|
str | None
|
Full OAuth scope string. Overrides |
None
|
project_id
|
str | None
|
Zitadel project id; expands to the audience scope. |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If neither |
Example
creds = OAuthClientCredentials( ... client_id="databaas-pipeline", ... client_secret=os.environ["CLIENT_SECRET"], ... token_url="https://auth.example.com/oauth/v2/token", ... project_id="123456789", ... )
effective_scope
property
effective_scope: str
scope if set, else the Zitadel project-audience scope.
auth_header
auth_header() -> str
Bearer <token>, fetching or refreshing the token if it is stale.
Raises:
| Type | Description |
|---|---|
AuthenticationError
|
If the token endpoint is unreachable or rejects the credentials. |
ServiceAccountKey
dataclass
ServiceAccountKey(
key_id: str,
private_key: str,
user_id: str,
token_url: str,
scope: str | None = None,
project_id: str | None = None,
audience: str | None = None,
_cache: dict[str, Any] = dict(),
_lock: Lock = threading.Lock(),
)
A Zitadel service-account JSON key: Private Key JWT (RFC 7523) auth.
Some Zitadel consoles only offer a downloadable JSON key for a machine
user, not a plain client secret — the key's fields are exactly
type/keyId/key/userId, matching :meth:from_json. This
signs a JWT assertion with the private key and exchanges it for a real
access token via the urn:ietf:params:oauth:grant-type:jwt-bearer
grant. The resulting token is a normal Zitadel-issued JWT — structurally
identical to what client_credentials with a secret produces, so
anything that accepts one accepts the other. Only how the client proves
itself to Zitadel differs; a Lakekeeper (or anything else) validating the
access token cannot tell the two apart.
A raw Zitadel Personal Access Token is not usable here or anywhere else against Lakekeeper: it is an opaque credential meant for calling Zitadel's own APIs, not a JWT a resource server's OIDC validator can parse. Signing an assertion and exchanging it for a real access token — what this class does — is the fix.
Needs the jwt extra: pip install "databaas-utils[jwt]".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key_id
|
str
|
The key's |
required |
private_key
|
str
|
The PEM private key (the JSON key's |
required |
user_id
|
str
|
The machine user's id (the JSON key's |
required |
token_url
|
str
|
The Zitadel token endpoint. |
required |
scope
|
str | None
|
Full OAuth scope string. Overrides |
None
|
project_id
|
str | None
|
Zitadel project id; expands to the audience scope. |
None
|
audience
|
str | None
|
The assertion's |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If neither |
Example
creds = ServiceAccountKey.from_json( ... "/secrets/zitadel-key.json", ... token_url="https://auth.example.com/oauth/v2/token", ... project_id="123456789", ... )
effective_scope
property
effective_scope: str
scope if set, else the Zitadel project-audience scope.
effective_audience
property
effective_audience: str
The assertion's aud: audience, else the issuer from token_url.
from_json
classmethod
from_json(
source: Mapping[str, Any] | str | PathLike[str],
**overrides: Any,
) -> ServiceAccountKey
Build from a Zitadel service-account JSON key file's own fields.
source is the file's parsed contents, its raw JSON text, or a path
to the file — accepting all three covers "I already loaded it" and "I
have the path" without asking the caller to pick one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Mapping[str, Any] | str | PathLike[str]
|
Parsed key, raw JSON text, or path to the JSON file. |
required |
**overrides
|
Any
|
The remaining constructor fields ( |
{}
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If the key lacks |
auth_header
auth_header() -> str
Bearer <token>, signing a new assertion if the token is stale.
Raises:
| Type | Description |
|---|---|
AuthenticationError
|
If the token exchange fails. |
ConfigurationError
|
If the private key cannot sign. |
databaas_utils.userauth
Interactive login as the user's own Zitadel identity.
Two flows, both ending in a refresh token: Authorization Code + PKCE through a
browser and a loopback redirect (interactive desktop use), and the OAuth2
Device Authorization Grant (SSH / headless, where nothing can reach a
loopback redirect). After the first login, auth_header() renews silently
from the refresh token; an interactive login only happens again once that
refresh token itself has expired or been revoked.
Mirrors the double-checked, expiry-aware caching in settings.py's
OAuthClientCredentials/ServiceAccountKey, but with two extra layers:
the cache is cross-process (the token store), and refreshing must be
serialised across processes too, since Zitadel rotates the refresh token on
every use.
UserLogin
dataclass
UserLogin(
issuer: str,
client_id: str,
project_id: str,
store_key: str,
interactive: bool = True,
flow: str = "auto",
redirect_port: int = 0,
store: TokenStore = default_store(),
)
A TokenProvider backed by a user's own interactive Zitadel login.
Built from a saved profile via UserLogin.from_profile — see
LakehouseSettings.from_profile, the normal entry point. auth_header()
serves a cached token, refreshes silently when a stored refresh token is
still good, and — only when interactive is true — falls back to an
interactive login when there is none.
scope
property
scope: str
The OAuth scope requested at login.
from_profile
classmethod
from_profile(
profile: Profile, **overrides: Any
) -> UserLogin
Build from a saved :class:~databaas_utils.Profile.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile
|
Profile
|
The profile to log in with. |
required |
**overrides
|
Any
|
Other fields, e.g. |
{}
|
auth_header
auth_header() -> str
Bearer <token>, refreshing or logging in first if needed.
Raises:
| Type | Description |
|---|---|
AuthenticationError
|
If there is no usable session and
|
invalidate
invalidate() -> None
Drop the in-memory access token only. Used by 401-retry logic.
login
login() -> None
Run an interactive login now, regardless of any cached token.
logout
logout() -> None
Revoke the stored refresh token (best effort) and forget it.
claims
claims() -> dict[str, Any]
Decode the current access token's payload, unverified.
The server has already verified it; this is only for local display
(databaas whoami), never for authorisation decisions.
userinfo
userinfo() -> dict[str, Any]
The signed-in user's profile (email, name) from the issuer.
Zitadel puts the email in the ID token and the userinfo response, not
in the access token that :meth:claims decodes. Only for display;
returns {} when the issuer has no userinfo endpoint or refuses.
login
login(
profile: str | None = None, **overrides: Any
) -> UserLogin
Log in interactively and return a live UserLogin bound to the profile.
Convenience for notebooks: opens a browser (or prints a device code),
persists the session, and returns something LakehouseSettings can be
built from directly — or just call LakehouseSettings.from_profile()
afterwards, which reuses the cache this writes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile
|
str | None
|
Profile name; the default profile if omitted. |
None
|
**overrides
|
Any
|
Passed to :meth: |
{}
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If no such profile exists. |
databaas_utils.jupyterhub.JupyterHubToken
dataclass
JupyterHubToken(
api_url: str,
api_token: str,
timeout: float = REQUEST_TIMEOUT,
)
A TokenProvider that asks the hub for the user's access token.
Most code should use databaas_utils.user instead of this directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
api_url
|
str
|
The hub API URL ( |
required |
api_token
|
str
|
The server's API token ( |
required |
timeout
|
float
|
Per-request timeout in seconds. |
REQUEST_TIMEOUT
|
endpoint
property
endpoint: str
The hub's oauth-token URL.
from_env
classmethod
from_env(
env: Mapping[str, str] | None = None,
) -> JupyterHubToken
Build from JUPYTERHUB_API_URL and JUPYTERHUB_API_TOKEN.
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If either variable is unset. |
auth_header
auth_header() -> str
Bearer <token>, asking the hub only when the cached token is stale.
Raises:
| Type | Description |
|---|---|
AuthenticationError
|
If the hub cannot supply a token. A 401 means the user's login expired; log out of Jupyter and back in. |
invalidate
invalidate() -> None
Drop the cached token so the next call asks the hub again.
databaas_utils.profiles
Named local profiles: which Zitadel issuer and Lakekeeper warehouse to use.
One environment — one Zitadel instance, one Lakekeeper — is one profile. A
machine can hold several (dev, a customer's prod, ...), selected with
--profile/DATABAAS_PROFILE, with one marked default. discover()
fills a profile in from the portal's public discovery document rather than
requiring the values to be typed in by hand.
Profile
dataclass
Profile(
name: str,
issuer: str,
client_id: str,
project_id: str,
catalog_url: str,
warehouse: str = DEFAULT_WAREHOUSE,
region: str = DEFAULT_REGION,
redirect_port: int = 0,
)
Everything needed to log in and reach one warehouse as one identity.
Profiles live in ~/.config/databaas/config.toml and are written by
databaas login. Most code loads one with :func:load_profile or, more
often, never touches it: LakehouseSettings.from_profile() does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Profile name. |
required |
issuer
|
str
|
The Zitadel issuer URL. |
required |
client_id
|
str
|
The Native app's client id. |
required |
project_id
|
str
|
The Zitadel project id, requested as an extra token audience. Empty relies on the app's own project being in the audience, which Zitadel adds by default. |
required |
catalog_url
|
str
|
The Lakekeeper server root. |
required |
warehouse
|
str
|
Warehouse name, |
DEFAULT_WAREHOUSE
|
region
|
str
|
Storage region. |
DEFAULT_REGION
|
redirect_port
|
int
|
Fixed loopback port for the browser login; |
0
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If a required field is empty. |
__post_init__
__post_init__() -> None
Reject a profile with an empty required field.
load_profile
load_profile(name: str | None = None) -> Profile
Resolve: an explicit name, then DATABAAS_PROFILE, else the default.
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If no profile is configured or the name is unknown. |
discover
discover(
portal_url: str, *, name: str | None = None
) -> Profile
Fetch a profile's settings from the portal's public discovery document.
portal_url is the environment's own address (e.g.
https://infoplaza.databaas.eu), not the Zitadel issuer or the catalog.
The document needs only client_id: the issuer and catalog default to
the auth. and catalog. addresses next to the portal, and the
warehouse to databaas-lakehouse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portal_url
|
str
|
The environment's portal address. |
required |
name
|
str | None
|
Profile name; the portal's first hostname label by default. |
None
|
Raises:
| Type | Description |
|---|---|
HTTPError
|
If the portal serves no discovery document. |
endpoints_from_base_url
endpoints_from_base_url(base_url: str) -> tuple[str, str]
The Zitadel issuer and Lakekeeper root of the environment at base_url.
A Databaas environment serves Zitadel on auth.<host> and Lakekeeper on
catalog.<host>, next to the portal on <host>.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str
|
The environment's portal address, e.g.
|
required |
Returns:
| Type | Description |
|---|---|
tuple[str, str]
|
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If |
Example
endpoints_from_base_url("https://next.dev.databaas.app") ('https://auth.next.dev.databaas.app', 'https://catalog.next.dev.databaas.app')