Skip to main content

fakts

codecov PyPI version Maintenance Maintainer PyPI pyversions PyPI status Ruff

Renamed. This client used to be published as fakts-next (imported as fakts_next). It is published as fakts now, imported as fakts; the fakts_next name is gone, so update both the dependency and the imports.

Fakts is an asynchronous app configuration and service-discovery client for dynamic client-server deployments. An app declares what it needs (a manifest with required services); fakts negotiates where and how to get it: it discovers the server, registers the app (with one-time user approval), fetches possible paths to find declared services, resolves every required service to a reachable address, and hands out OAuth2 tokens — caching everything so all of this happens exactly once.

Fakts powers app configuration for the Arkitekt platform, but contains no Arkitekt-specific logic: it speaks a small, documented HTTP protocol that any server can implement.

Install

pip install fakts

Optional extras:

pip install "fakts[rath]"    # FaktsAuthLink: rath GraphQL requests carry fakts tokens
pip install "fakts[crypto]"  # signed alias challenges (Ed25519 verification)
pip install "fakts[mesh]"    # reach services that are only on the deployment's mesh

Quickstart

from fakts import build_device_code_fakts, Manifest, Requirement

fakts = build_device_code_fakts(
    url="http://localhost:8000",
    manifest=Manifest(
        identifier="my-app",
        version="0.1.0",
        scopes=["openid"],
        requirements=[
            Requirement(key="rekuest", service="live.arkitekt.rekuest"),
        ],
    ),
)

async with fakts:
    alias = await fakts.aget_alias("rekuest")   # a verified, reachable address
    url = alias.to_http_path("graphql")         # http(s)://host:port/path/graphql
    ws = alias.to_ws_path("graphql")            # ws(s)://...
    token = await fakts.aget_token()            # OAuth2 access token

The first run opens the browser: the server shows what my-app is asking for, the user approves it once, and the granted configuration is cached in .fakts_cache.json. Every later run starts instantly and silently from the cache — until the manifest changes (new scopes or requirements automatically invalidate the cache) or the cached services stop answering (fakts then re-fetches the configuration once and heals itself).

Everything also works synchronously (via koil) — same instance, no asyncio boilerplate:

with fakts:
    alias = fakts.get_alias("rekuest")
    token = fakts.get_token()

Core concepts

Manifest & requirements

The Manifest is your app's identity card: a globally unique identifier, a version, the OAuth2 scopes it needs, and a list of Requirements — the services it wants to talk to, referenced by a key of your choosing and a globally unique service identifier (reverse-domain style). Requirements can be optional=True: the app keeps working when they are absent.

The remote protocol: discover → authorize

A RemoteGrant is composed of two pluggable parts:

Role Question it answers Implementations
Discovery Where is the coordination server? WellKnownDiscovery (/.well-known/fakts), StaticDiscovery
Authorizer How do we get a session? DeviceCodeAuthorizer (browser approval), MandateKeyAuthorizer (a standing mandate key, headless), StaticAuthorizer (a credential from an earlier session)

Protocol v1 had a third role: a claimer that traded an approval artifact for the configuration. It is gone because the OAuth token endpoint returns both at once — there is nothing left to trade.

The builders (build_device_code_fakts, build_mandate_key_fakts) wire the common combinations for you; compose RemoteGrant yourself for anything exotic. The exact HTTP exchanges are specified in The Fakts protocol below.

ActiveFakts: what the server grants

The granted configuration contains the deployment's identity (self), the OAuth2 session held for your app (auth — a client_id and a rotating refresh token, never a client secret), and one Instance per service, each with a list of Aliases — candidate addresses for reaching that service (a deployment may expose the same service on a LAN address, a VPN address and a public address).

instances only ever contains services that were actually granted. The sibling statuses map reports the outcome per requirement key (granted / denied / unavailable), so a user declining an optional service is distinguishable from a deployment that does not offer it. Servers that do not report statuses simply omit the field; the client then treats missing services as unknown.

match await fakts.aget_grant_status("kabinet"):
    case GrantStatus.DENIED: ...       # the user said no — respect it
    case GrantStatus.UNAVAILABLE: ...  # the deployment can't offer it
    case GrantStatus.GRANTED: ...

# or simply, for graceful degradation:
if alias := await fakts.aget_alias_or_none("kabinet"):
    enable_kabinet_features(alias)

Alias resolution: challenge once, then stick

On the first aget_alias(key), fakts challenges the aliases of every required service (a concurrent HTTP probe against each alias's challenge path) and keeps the first one that answers. If the instance carries a challenge_key, the probe is cryptographically verified: the service must sign a fresh nonce with its identity key, so an impostor answering 200 does not pass (see the protocol section). Subsequent calls return the selected alias instantly — no further probing. The winning alias is also persisted as the preferred one, so the next process start challenges the last-known-good address first. Pass force_refresh=True to re-resolve, or omit_challenge=True to skip probing entirely.

Within one service, that last-known-good alias is asked first and alone. If it answers within alias_head_start (0.3 s) nothing else is asked; if not, the other aliases are challenged alongside it and the first to pass is used, so aliases that do not answer cost one alias_challenge_timeout between them, not one each. A mesh node is only started once the direct aliases have failed — or, for a service last reached over the mesh, once the head start has passed without one of them answering.

mesh=MeshOptions(force=True) (or MeshProxy(url=..., force=True)) turns that around: only mesh aliases are used, the node starts at once, and a service without a working mesh alias is unreachable — for proving that a deployment works over its mesh, or where the direct addresses must not be used.

When no alias of a service passes, the error says what was tried:

try:
    alias = await fakts.aget_alias("mikro")
except CompositionError as e:           # a required service
    for failure in e.failures:
        for attempt in failure.attempts:
            print(attempt.alias_id, attempt.url, attempt.outcome, attempt.describe())
except ServiceUnreachableError as e:    # an optional one; e.failure
    ...

Each attempt has an AttemptOutcome (TIMEOUT, UNREACHABLE, TLS, BAD_STATUS, UNSIGNED, BAD_SIGNATURE, MESH_UNAVAILABLE, ...); a signature that does not verify is also logged as a warning when another alias works. A service found down is not asked again for alias_retry_after (5 s): lookups within it raise the same error at once, and after it only that service is challenged again.

Caching & self-healing

The cache (FileCache by default in the builders) stores the granted session across runs, keyed to a hash of your manifest and the server url — change either and the cache invalidates itself. If a cached configuration turns out to be stale (services moved), fakts reloads from the grant once and retries before failing.

Under protocol v2 the cache is no longer just configuration: it holds a live refresh token that rotates on every renewal, so it is written at mode 0600 and updated far more often than before. Two consequences are worth knowing:

  • Give each app its own cache path. The default is relative to the working directory, which is rarely what you want for anything but a script.
  • Processes sharing a cache cooperate rather than compete. Each rotation revokes its predecessor, so a process whose token was rotated away adopts the one it finds on disk instead of re-authorizing — which would otherwise invalidate its siblings' credentials too.

When a session cannot be renewed at all — the authorization was revoked, or it hit the server's maximum age — fakts raises NeedsReauthenticationError rather than silently opening a browser, because re-approving an app replaces its registration and disconnects every other process using it. Catch it and call fakts.alogin() at a point where prompting is appropriate:

try:
    token = await fakts.aget_token()
except NeedsReauthenticationError:
    await fakts.alogin()     # prompts only if the session cannot be revived
    token = await fakts.aget_token()

alogin() is idempotent — a healthy session returns immediately without a prompt. arefresh() is the blunter tool: it always re-runs the grant, which replaces the app's client registration and disconnects sibling processes.

To end a session, alogout() revokes it and forgets it locally. A server that advertises a revocation_endpoint in its well-known document (RFC 7009) is told first, so the refresh token stops renewing for every holder of a copy; the access token already issued ages out by itself. Against a server that advertises none, the session is only forgotten here, and a sibling process still holding it will re-persist it on its next rotation.

The Fakts protocol

Everything a server needs to implement to speak fakts. This section describes protocol version 2, which is an extension of the OAuth 2.0 device authorization grant (RFC 8628) rather than a protocol of its own. The server advertises the version it speaks in the well-known document (protocol_version); clients treat a missing value as "1" and refuse to continue, since v1 and v2 share no endpoints.

What is genuinely fakts-specific — service instances, aliases, signed alias challenges, per-requirement consent and grant statuses — rides along as extension members on otherwise ordinary OAuth messages. Everything else is standard, so an off-the-shelf OAuth library recognises most of the exchange.

discover ── GET  {url}/.well-known/fakts          Where is the server?
demand   ── POST {device_authorization_endpoint}  Register + stage a user code
consent  ── browser at verification_uri_complete  One-time user approval
token    ── POST {token_endpoint}, polled         Tokens *and* configuration
renew    ── POST {token_endpoint}                 grant_type=refresh_token
use      ── alias challenges, report endpoint

Two properties of the token endpoint shape everything downstream:

  • It returns the configuration together with the tokens. There is no separate "claim" step, because there is no intermediate artifact to trade.
  • It re-renders that configuration on every response, including every refresh. Aliases are host-aware, so configuration drift reaches clients without anyone re-approving anything.

1. Discovery — GET {url}/.well-known/fakts

Returns a document that is simultaneously the fakts descriptor and OAuth 2.0 authorization-server metadata:

{
  "name": "My Deployment",
  "version": "0.1.0",
  "protocol_version": "2",
  "description": "...",
  "base_url": "https://example.com/lok/f/",

  "issuer": "https://example.com",
  "token_endpoint": "https://example.com/lok/o/token/",
  "device_authorization_endpoint": "https://example.com/lok/o/app-authorization/",
  "jwks_uri": "https://example.com/lok/o/jwks/",
  "grant_types_supported": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_basic"],
  "configure": "https://example.com/configure/{code}"
}

name and token_endpoint are required. The fakts members carry no prefix — they sit alongside the standard ones in the same flat object, and unknown members must be ignored rather than rejected (the same document also advertises mesh_* and hub_* endpoints that most clients skip).

Every endpoint URL is absolute. Deployments commonly sit under a script-name prefix (/lok above), so a client that rebuilds an endpoint by appending to issuer will get the path wrong. Take the URLs as given.

The one exception is the report endpoint, which is not advertised; clients derive it as {base_url}report/.

2. Demand — POST {device_authorization_endpoint}

A JSON body, not form encoding — this is where v2 extends OAuth, because OAuth has no slot for "here is what my app needs, ask the user which parts to grant":

{
  "manifest": {"identifier": "my-app", "version": "0.1.0", "scopes": ["openid"],
               "requirements": [{"key": "rekuest", "service": "live.arkitekt.rekuest",
                                 "optional": false}]},
  "expiration_time_seconds": 300,
  "redirect_uris": [],
  "requested_client_kind": "development",
  "requested_client_role": "interface"
}

The manifest is a nested object here. (The mandate-key grant in §5 sends the same manifest as a JSON string in a form field — the encodings genuinely differ, so do not share a code path between them.)

This request also performs client registration: the server mints a public OAuth client for the app (client_secret: "", token_endpoint_auth_method: "none") and binds it to the device code when the user approves. One client per app, not one per installation — under v2 it is the token that identifies an installation.

The response is RFC 8628 plus the minted client_id:

{
  "status": "granted",
  "device_code": "...", "user_code": "WDJB-MJHT", "client_id": "6f0c...",
  "token_endpoint": "https://example.com/lok/o/token/",
  "verification_uri": "https://example.com/configure/",
  "verification_uri_complete": "https://example.com/configure/WDJB-MJHT",
  "expires_in": 300, "interval": 5
}

Clients must carry client_id forward — into polling, and into every later refresh. It is never derived from the manifest identifier. They should send the user to verification_uri_complete as given rather than deriving an approval URL.

Two error shapes are specific to this endpoint and easy to mistake for success: failure is reported as HTTP 200 with {"status": "error", "error": "..."}, and throttling as HTTP 429 with a bare {"error": "slow_down"} and no status key at all.

3. Token — POST {token_endpoint}, polled

Standard RFC 8628 polling, form-encoded:

grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=...&client_id=...

The RFC 8628 error codes replace v1's bespoke status envelope:

error client behaviour
authorization_pending keep polling
slow_down interval += 5, keep polling
access_denied the user refused — a legitimate outcome, not a fault
expired_token the code expired before approval

Success carries the tokens and the configuration as top-level members:

{
  "access_token": "eyJ...", "refresh_token": "...", "token_type": "Bearer",
  "expires_in": 3600, "scope": "openid read",
  "client_id": "6f0c...",

  "self": {"deployment_name": "my-deployment",
           "alias": {"id": "self", "host": "example.com", "ssl": true, "path": "lok", "challenge": "ht"}},
  "instances": {
    "rekuest": {"service": "live.arkitekt.rekuest", "identifier": "3",
                "aliases": [{"id": "lan", "host": "10.0.0.4", "port": 8080, "challenge": "ht"}],
                "challenge_key": {"kind": "ed25519", "key": "<base64>"}}
  },
  "statuses": {"rekuest": "granted", "kabinet": "denied"}
}

instances is keyed by the requirement key from the manifest, not by service identifier. statuses is granted | denied | unavailable; denied means the user declined an optional requirement, unavailable that the deployment could not offer it. Unknown values must not break older clients.

The granted scope may legitimately be narrower than what was requested — that is what per-requirement consent means — so clients must not validate one against the other.

4. Renew — grant_type=refresh_token

grant_type=refresh_token&refresh_token=...&client_id=...

Both parameters are required: the endpoint authenticates the client before it validates the token, and then checks the token belongs to that client. A bare refresh token is not a usable credential.

Refresh tokens rotate: each use issues a new one and revokes its predecessor immediately. This has consequences that are easy to get wrong, so they are stated as obligations below.

5. Mandate key — the headless grant

For CI runners, deployed containers and the apps a hub or a deployer runs, where no human is available. A mandate is an authority someone granted in advance; whoever holds its key logs the app in as them. An extension grant at the same token endpoint:

grant_type=urn:fakts:grant-type:mandate-key&key=...&manifest=<json string>
  [&requested_client_kind=development|desktop|mobile][&requested_client_role=interface|agent][&request_auth_key=true]

Note the URN has no params:oauth segment — it is a fakts URN. Returns the same envelope as §3 but no refresh token: the client logs in again with the key whenever its access token runs out, and the server checks the mandate every time, so a revoked key stops working within one access-token lifetime.

6. Report — POST {base_url}report/

Optional, best-effort telemetry that lets the server flag broken compositions. Authenticated with the access token, like any other resource:

Authorization: Bearer <access_token>
{
  "alias_reports": {"rekuest": {"alias_id": "lan", "reason": null, "valid": true}},
  "functional": true
}

Keyed by requirement key. A failing report must never break the app.

7. Using the configuration

Alias challenges are unchanged from v1. Each alias advertises a challenge path; the client GETs it and requires a 200 before treating the alias as usable, trying them in order until one answers.

When an instance carries a challenge_key, a plain 200 is no longer enough. The client sends a random nonce and the service must return a signature over the domain-separated message fakts-challenge-v1:<nonce>, made with the matching Ed25519 private key:

GET {alias.challenge_path}?nonce=<random>
→ {"signature": "<base64>"}

The client verifies against the pinned key and never downgrades: an instance with a key that answers without a valid signature is rejected, which is what stops anything on the network from impersonating a service by answering first.

Client and server obligations

Rotation only works if both sides hold up their end. These are normative.

Servers MUST NOT apply refresh-token reuse detection to fakts clients, or MUST accept a superseded token for a short grace window. Fakts clients are public, share a process-local cache, and legitimately race. OAuth 2.1 recommends reuse detection with family-wide revocation; applying it here turns a recoverable collision into a hard failure that needs a human.

Clients MUST persist a rotated refresh token before using the new access token, at mode 0600. The server commits the rotation when it answers, so a token that is used but not persisted is simply lost.

Clients MUST NOT re-run an interactive grant automatically. Approving an app again causes the server to replace its client registration and delete the old one — which severs every other process sharing that credential. Automatic recovery is only safe for non-interactive grants (a mandate key).

Multi-process deployments SHOULD share one cache and let the loser of a rotation race adopt the winner's credential, rather than each re-authorizing.

Transport

Plain HTTP against a loopback host is always acceptable, and needs no configuration on either side. Plain HTTP against a network host is a supported deployment mode, but since v2 puts a rotating refresh token on the wire it must be opted into explicitly on both ends — clients via allow_insecure_transport (or FAKTS_ALLOW_INSECURE_TRANSPORT=1), servers via whatever their OAuth library requires.

Design notes

Most of fakts is machinery for negotiating configuration that a smaller app would simply hardcode. Each piece is there because the obvious simpler choice breaks in a real client–server deployment. The decisions worth knowing:

Negotiation instead of static configuration. The obvious approach is to bake service URLs into the app (or read them from a config file). But in a client–server world the deployment owns the topology: the same service may live on a LAN address, a VPN address and a public one; services move; each deployment mints its own OAuth2 client. So the app declares what it needs (the manifest) and the deployment decides where — fakts negotiates once and caches the result. When you genuinely don't need negotiation (config injected by a container, or hardcoded in a test) the static path is still there: see EnvGrant and HardFaktsGrant.

RemoteGrant is two pluggable parts, not one. The remote flow could be a single object, but its two questions vary independently: where is the server (well-known URL, static) and how do we get a session (device-code browser flow, a mandate key, an existing credential). Splitting Discovery / Authorizer into runtime-checkable protocols lets you compose new combinations — and implement a part in your own code — without touching the orchestration. See discover → authorize.

It used to be three parts: protocol v1 separated getting approved from fetching the config, because approval yielded a claim token that a second request exchanged. Adopting the OAuth device grant collapsed both into the token endpoint, and the third part had nothing left to do.

Challenge once, then stick. Two tempting extremes are both wrong: re-probing every alias on every aget_alias is slow (each probe carries a timeout) and churny, while trusting the cached address blindly hands back dead endpoints when a service moves. fakts instead challenges all required services once on first resolution, keeps the first alias that answers, and returns it instantly thereafter — persisting it as the preferred address so the next process start tries the last-known-good first. Re-resolution happens only on force_refresh=True or the cached-config self-heal path. See alias resolution.

Signed challenges are opt-in per service instance. Always requiring signatures would force every deployment — including legacy ones and TLS-only services whose certificate already proves identity — to mint and manage an Ed25519 key. Never allowing them means a host that merely answers 200 on a shared network can impersonate a service. Per-instance opt-in puts the bar exactly where a deployment wants it, and once a key is pinned there is no silent downgrade to a plain 200. The honest limit: over plain http a signature authenticates the probe, not the channel — use ssl: true aliases for real channel security. See using the configuration.

The cache key is a hash of the manifest and the server URL. A single path-keyed cache file would silently serve stale data across two boundaries that matter: a changed manifest is a changed permission boundary (new scopes or requirements), and a changed URL is a different deployment (dev vs prod). Hashing both means either change invalidates the cache automatically. Writes are atomic (temp file + rename) and a corrupt cache is treated as a miss and reloaded — caching is an optimization, never a thing that can break startup. See caching & self-healing.

Denial is a first-class outcome, not an error. If a user declining an optional service looked the same as an outage, app authors would mark everything required and users would grant everything — defeating consent. So the server's per-key statuses distinguish granted / denied / unavailable, ServiceNotGrantedError (a subclass of AliasNotFoundError) is catchable, and aget_alias_or_none lets an app degrade gracefully. The end user stays in control of what an app may reach. See what the server grants.

One instance, sync and async. Rather than ship two clients that drift apart, fakts writes the async methods (aget_alias, aget_token, …) and derives the synchronous ones from them via koil, so both surfaces share a single implementation and a single event loop.

Recipes

Headless / CI: mandate key

No browser available? Have someone issue a mandate key (kontrol, a hub, a deployer, the deployment config) and use:

from fakts import build_mandate_key_fakts

fakts = build_mandate_key_fakts(
    url="http://localhost:8000",
    manifest=manifest,
    key="my-mandate-key",
)

Containers: configuration from the environment

When the configuration is provisioned from the outside (compose files, mounted secrets), skip the server negotiation entirely:

from fakts import Fakts, EnvGrant

# reads $FAKTS (inline JSON) or $FAKTS_FILE (path to a JSON file)
fakts = Fakts(grant=EnvGrant(), manifest=manifest)

Testing: no server at all

build_testing_fakts maps each key to a URL (or a full Alias). Every key becomes a requirement and a granted instance, aliases resolve without being challenged, and tokens come from a list:

from fakts import build_testing_fakts

fakts = build_testing_fakts(
    {"rekuest": "http://localhost:8090"},
    tokens=["first", "second"],   # handed out in order; the last one repeats
    token_lifetime=0,             # 0: every aget_token() renews; None: never expires
)

async with fakts:
    alias = await fakts.aget_alias("rekuest")
    assert await fakts.aget_token() == "first"
    assert fakts.token_fetches == 1

The embedded auth points at a .invalid host, so anything that escapes the testing seams and tries a real refresh fails loudly instead of reaching a server. For a configuration you already have as an ActiveFakts, use Fakts(grant=HardFaktsGrant(fakts=...), manifest=...) from fakts.grants.hard.

GraphQL via rath

With the [rath] extra, fakts.contrib.rath.auth.FaktsAuthLink puts the access token on every request and renews it once when a request is rejected. It takes a TokenLoader — anything with aget_token() and arefresh_token(stale_token), which a Fakts is:

from fakts.contrib.rath.auth import FaktsAuthLink

auth = FaktsAuthLink(token_loader=fakts)

The renewal never prompts: a rejected request raises NeedsReauthenticationError rather than opening a browser mid-request.

Mesh-only services

Some deployments put services on a private mesh (a tailnet) instead of a public address. The server marks those aliases kind: "mesh". By default the builders use the mesh when it is available, and stay quiet when it is not:

  • Without the bindings (pip install "fakts[mesh]") the mesh is off, and mesh aliases are skipped.
  • With them, the login asks the server for a key to join the mesh with. The server may grant none (the user opted out of the mesh, or the organization has none), and then mesh aliases are skipped, too.
  • A node is only started when a service is reachable no other way: while no node is running, a service's mesh aliases wait until its other aliases have failed. Later runs reuse the node's state (under ~/.local/state/arkitekt/mesh on Linux).

Choose explicitly with mesh:

from fakts import MeshOptions, MeshProxy, build_device_code_fakts

build_device_code_fakts(url=url, manifest=manifest)                           # MeshOptions(auto=True)
build_device_code_fakts(url=url, manifest=manifest, mesh=MeshOptions())       # the same, but report what is missing
build_device_code_fakts(url=url, manifest=manifest, mesh=MeshProxy(url="http://localhost:1055"))  # a running proxy
build_device_code_fakts(url=url, manifest=manifest, mesh=None)                # off

A mesh alias comes back with proxy set, so an HTTP client that honours it reaches the service over the mesh. For traffic that cannot go through an HTTP proxy, an alias resolved through a node this process runs also offers:

local = await alias.aforward()      # "127.0.0.1:P", forwarding TCP to the alias
ice = await alias.aturn()           # TurnInfo: the node's TURN relay, as an ICE server

Both raise arkitekt_spec's MeshError for an alias reached through a MeshProxy (there is no node to forward through) or one that is not on the mesh; fakts.MeshError is a subclass of it, so catching that one catches both. If the node cannot start or join, only mesh aliases are affected: their resolution error names why, every non-mesh alias keeps resolving, and fakts does not retry the join on every call.

build_mandate_key_fakts takes mesh too, and request_auth_key=True asks for a join key on the first login (the mandate must allow it); otherwise a node only comes up if it already joined on this machine, or use a MeshProxy.

Error handling

All errors derive from FaktsError and carry the URL contacted, the status code and (truncated) response body where applicable:

Error Raised when
NotEnteredError A method needing the context was called outside with/async with
GrantError / RemoteGrantError The grant could not load the configuration: DiscoveryError (finding the server), DemandError and its subclasses UserDeniedError, DeviceCodeExpiredError, DeviceCodeTimeoutError, RetrieveError (getting a session)
CompositionError One or more required services could not be resolved to a working alias
AliasNotFoundError aget_alias(key) for a key that is not resolvable (not in the manifest, or its challenges failed)
ServiceNotGrantedError Subclass of AliasNotFoundError: the key is declared, but the server granted no instance (user declined, or service unavailable) — catch it (or use aget_alias_or_none) to degrade gracefully
NeedsReauthenticationError The session can only be recovered by a human — call alogin() where prompting is appropriate
MeshError The mesh node could not start or join (its code is the node's own: needs_login, locked, timeout, ...; None when fakts refused, e.g. the [mesh] extra is missing). A subclass of arkitekt_spec's MeshError, which aforward()/aturn() raise for an alias with no node of this process behind it

Fakts options

Option Default Effect
manifest required What the app is and which services it requires
grant required Where the configuration comes from (RemoteGrant, EnvGrant, HardFaktsGrant, ...)
cache NoCache() Where the session is kept across runs; the builders pass a FileCache
ssl_context certifi's CAs TLS verification for every request fakts makes
allow_insecure_transport False Permit sending credentials over plain http to a non-loopback host (loopback never needs it)
reauth_policy ReauthPolicy.ON_LOGIN When token renewal may run an interactive grant: NEVER, ON_LOGIN (only alogin()/aload()/arefresh() may prompt), or ALWAYS
mesh None (builders: MeshOptions(auto=True)) MeshOptions() or MeshProxy(url=...) to reach mesh-only aliases (see mesh-only services)
allow_auto_load True If False, aget_* raises instead of loading implicitly — call aload() yourself
refetch_on_alias_failure True Reload from the grant once when aliases from a cached config fail their challenges — only when that needs no human; otherwise it raises and asks you to alogin()
alias_challenge_timeout 3 Seconds per alias challenge probe
delete_on_exit False Reset the cache and loaded state on exit

build_device_code_fakts and build_mandate_key_fakts set most of these for you. Beyond url and manifest they take cache_file, no_cache, ssl_context, allow_insecure_transport and mesh; the device-code builder also takes headless, timeout, requested_client_kind and requested_client_role.

Development

uv sync --all-extras --dev                         # install (Python >= 3.11)
uv run pytest -m "not integration"                 # unit tests, no docker
uv run pytest                                      # everything; needs docker (Fakts server, mesh lab)
uv run ruff check && uv run ruff format --check    # lint (blocking in CI)
uv run basedpyright                                # types (blocking in CI)

Metadata

Release files for fakts 6.0.0

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

Source distribution (sdist)

Source distribution for fakts 6.0.0
File Size Uploaded
fakts-6.0.0.tar.gz 106.9 kB Details

Built distribution (wheel)

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

Total release size: 220.6 kB

Release files / fakts-6.0.0.tar.gz

Download URL fakts-6.0.0.tar.gz
Size 106.9 kB
Tags Source
SHA-256 checksum
How to use checksums
db5c5a932d63bf1f46d9cf55a24107f4ea15aedb931c5dd0a3f133ff29c7c528
BLAKE2b-256 checksum
How to use checksums
ddd5585e77c0aa598a336473809610eca25601c25099750c9772ff58080856ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / fakts-6.0.0-py3-none-any.whl

Download URL fakts-6.0.0-py3-none-any.whl
Size 113.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
402ad6b2bc14a64599df1ccd333c4a8f95a6206db8b2a111c13da539922ed6a1
BLAKE2b-256 checksum
How to use checksums
e5ec086fd2c53a422d15d5ef0e49199f1a8f0079013e152acfa336e58fe6e64a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

6.0.0 This release

2 release files

5.5.0

2 release files

5.4.0

2 release files

5.3.0

2 release files

5.2.0

2 release files

5.1.0

2 release files

5.0.0

2 release files

4.1.4

2 release files

4.1.3

2 release files

4.1.2

2 release files

4.1.1

2 release files

4.1.0

2 release files

4.0.0

2 release files

3.0.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.0.0

2 release files

0.5.0

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.47

2 release files

0.3.46

2 release files

0.3.45

2 release files

0.3.44

2 release files

0.3.43

2 release files

0.3.42

2 release files

0.3.41

2 release files

0.3.40

2 release files

0.3.39

2 release files

0.3.38

2 release files

0.3.37

2 release files

0.3.31

2 release files

0.3.30

2 release files

0.3.29

2 release files

0.3.28

2 release files

0.3.27

2 release files

0.3.26

2 release files

0.3.24

2 release files

0.3.20

2 release files

0.3.19

2 release files

0.3.18

2 release files

0.3.17

2 release files

0.3.16

2 release files

0.3.11

2 release files

0.3.10

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.13

2 release files

0.2.12

2 release files

0.2.11

2 release files

0.2.8

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.1.47

2 release files

0.1.46

2 release files

0.1.45

2 release files

0.1.44

2 release files

0.1.43

2 release files

0.1.40

2 release files

0.1.39

2 release files

0.1.35

2 release files

0.1.34

2 release files

0.1.33

2 release files

0.1.18

2 release files

0.1.17

2 release files

0.1.16

2 release files

0.1.15

2 release files

0.1.14

2 release files

0.1.13

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

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