fakts
Renamed. This client used to be published as
fakts-next(imported asfakts_next). It is published asfaktsnow, imported asfakts; thefakts_nextname 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/meshon 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)
| File | Size | Uploaded | |
|---|---|---|---|
| fakts-6.0.0.tar.gz | 106.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|