Skip to main content

wexa

Python client for the Wexa Fabric api-gateway. One module, stdlib only, no dependencies to conflict with yours.

Every call goes through the gateway's ten-stage governed lifecycle, so the same things happen to a graph query and to an agent run: the credential is checked, the grant is checked, quota is counted, arguments are validated, policy runs, a human approves if the data is sensitive, then it executes and is recorded. When something refuses, the error names the stage that refused it — S2:resolve-scope, S5:policy, S8:execute — so you know whether to fix your token, your payload, or your retry loop.

pip install wexa          # once published
pip install -e .          # from this repo today

Requires Python 3.9+.


60-second quickstart

1. Get a key. In the console: Simple mode → Generate API key. Keys are project-scoped and shown once.

2. Put it in the environment.

export WEXA_WORKSPACE=https://fabric.wexa.ai
export WEXA_API_KEY=fab_sk_...

3. Make a call.

from wexa import Fabric

fabric = Fabric()          # reads WEXA_WORKSPACE / WEXA_API_KEY
print(fabric.whoami())
{'user_id': 'usr_311', 'role': 'DEVELOPER', 'org_id': 'org_7f2a',
 'dept_id': '', 'project_id': 'prj_1c94',
 'grants': ['fabric:query.read', 'fabric:docs.read']}

That is the whole setup. The client discovers its endpoints from /v1/connection-info, so the workspace URL is the only address you supply. You can pass credentials inline instead, which is handy in a notebook:

fabric = Fabric(workspace="https://fabric.wexa.ai", api_key="fab_sk_...")

A client is bound to one project. Scope is fixed into the key when it is minted and never widens. To work across projects, make one client per key.

What to do next

WEXA_WORKSPACE=... WEXA_API_KEY=... python3 examples/status.py

status.py prints your role, which tools your grants actually cover, how close you are to the rate limit, and any approvals waiting on you. It is the fastest answer to "why did that call fail".


Authentication: what exactly do you send?

One HTTP header, on every call:

Authorization: Bearer <your credential>

That is all the client ever sends. Fabric(api_key=...) puts whatever string you give it behind Bearer , so the parameter name is narrower than the truth — any bearer credential the gateway accepts works there. The gateway names its two options itself, at the unauthenticated /v1/connection-info endpoint:

"auth": "OAuth 2.1 (PKCE) or Bearer API key (fab_sk_…)"
Credential Looks like Where it comes from Use it for
API key fab_sk_… Console → Simple mode → Generate API key scripts, backends, CI — anything long-running
Access token a JWT, three dot-separated blobs the OAuth 2.1 (PKCE) flow, or your identity provider apps acting on behalf of a signed-in person

Both go in the same header and behave identically once issued. Start with an API key; it needs no flow.

A credential is a key card, not a password

It does not just prove who you are. It carries where you may go and what you may do, stamped in at the moment it was minted:

  • Scope — one org, one department, one project. Printed on the card.
  • Grants — the specific doors it opens, like fabric:query.read.

Two consequences that surprise people:

  1. Scope never widens. A client is bound to one project for its whole life. To reach a second project, mint a second credential and build a second client. There is no "switch project" call, by design.
  2. Being authenticated is not being authorized. A perfectly valid credential still gets refused if it lacks the grant for the tool you called.

The failure you will actually hit

Missing grants are the most common auth error, and the message says exactly what is missing:

ForbiddenError: S2:resolve-scope — token missing required grant "fabric:ontology.write"

S2 is the scope stage, so you know the credential was accepted (S1 passed) and the permission is what's wrong. Fix the key, not the payload.

Which grant does each call need?

Six grants cover every tool in this client:

Grant Opens
fabric:query.read query_context, search_code, fetch_code, connector_read
fabric:ontology.write save_context, create_ontology
fabric:docs.read docs
fabric:orchestrate.read list_skills, get_process_flow, get_execution, knowledge_base_retrieve
fabric:orchestrate.write create_process_flow, update_process_flow, create_agent
fabric:agent.run run_agent, run_process_flow

Reads and writes are separate grants, and running something is separate from authoring it — a credential can be allowed to run a process flow without being allowed to change it.

Three other grants exist (fabric:catalog.read, fabric:catalog.write, fabric:codesync.write) but belong to endpoint families this client does not wrap. You will not need them here.

To see your own, ask:

print(fabric.whoami()["grants"])
# ['fabric:query.read', 'fabric:ontology.write']

Your first real call

Reads go through query_context, which runs read-only Cypher. One rule: the query must constrain itself to your project, or the gateway's Cypher guard rejects it before it reaches the graph.

rows = fabric.query_context(
    query="MATCH (c:Customer) WHERE c.project_id = $project_id RETURN c LIMIT 50",
)

$project_id is bound for you from the token. Do not pass project_id yourself — see Arguments the server owns.

Needs fabric:query.read.


What goes in a payload?

Every call is fabric.<tool>(**keyword_arguments). There is no envelope to build and no request object to construct — the keywords are the payload.

fabric.query_context(query="MATCH (n) WHERE n.project_id = $project_id RETURN n")
#                    └──────────── this is the whole payload ────────────┘

You supply the intent; the server supplies the identity

Some arguments are server-owned. They describe who is calling, and the gateway fills them in from your credential. Sending them yourself is refused — not ignored — because a payload that could name its own caller is a payload that could impersonate one.

You always send The server always fills in
query, nodes, relationships, goal, topic project_id, organization_id, executed_by
# refused before it leaves your process
fabric.query_context(query="…", project_id="prj_other")

# correct — reference it as a bound parameter instead
fabric.query_context(query="MATCH (n) WHERE n.project_id = $project_id RETURN n")

$project_id is already bound for you. Think of it as a blank the server fills in after you hand the form over. The complete list, and why the client refuses these rather than letting the server quietly overwrite them, is in Arguments the server owns.

Nodes: key names the identity field, it is not the identity value

This is the one shape worth reading twice, because key sounds like it holds an id and it does not — it holds the name of the property that holds the id.

{"label": "Refund", "key": "id", "properties": {"id": "rfnd_1001", "amount": 4200}}
#                    │                          └── the identity value
#                    └── the property to identify by

Read it as: "identify this Refund by its id property." That is what makes writes idempotent — send the same node twice and Fabric updates it rather than creating a duplicate, because it knows which field to match on. Point key at a property that is genuinely unique, and never at something like amount.

Relationships: endpoints are objects, not strings

A relationship has to say which node it means, and a bare "rfnd_1001" is ambiguous — two labels could each have a node with that id. So each endpoint repeats the address in full: {label, key, value}.

{"type": "ISSUED_TO",
 "from": {"label": "Refund",   "key": "id", "value": "rfnd_1001"},
 "to":   {"label": "Customer", "key": "id", "value": "cust_8841"}}

Note value here, versus properties on a node: a node carries its data, an endpoint only points at it.

When a payload is wrong, S4 tells you

Validation is stage 4, so a malformed payload never reaches the graph:

ValidationError: S4:validate-input … cannot unmarshal string into Go struct
field Rel.relationships.from of type contextsvc.Ref

Ref is that {label, key, value} object. This exact error means an endpoint was passed as a string.


Common tasks

Every sample below is one continuous scenario: a refund issued to a customer. Copy any of them as-is.

Write to the context graph

fabric.save_context(
    nodes=[
        {"label": "Refund", "key": "id",
         "properties": {"id": "rfnd_1001", "amount": 4200, "currency": "usd"}},
    ],
)

A node is {"label", "key", "properties"}, where key names the property that is the node's identity — key: "id" means properties.id is the identity.

Relationship endpoints are objects, not strings. This is the single most common payload error:

fabric.save_context(
    nodes=[
        {"label": "Refund", "key": "id", "properties": {"id": "rfnd_1001"}},
        {"label": "Customer", "key": "id", "properties": {"id": "cust_8841"}},
    ],
    relationships=[
        {"type": "ISSUED_TO",
         "from": {"label": "Refund", "key": "id", "value": "rfnd_1001"},
         "to": {"label": "Customer", "key": "id", "value": "cust_8841"}},
    ],
)

Passing "from": "rfnd_1001" gets you:

ValidationError: S4:validate-input ... cannot unmarshal string into Go struct
field Rel.relationships.from of type contextsvc.Ref

save_context needs a project in Simple/auto mode, which is the default. On an Advanced/manual project it fails validation outright and create_ontology is the ingestion path instead — see Project mode.

Needs fabric:ontology.write.

Run an agent or a process flow

These start real work and return without waiting for it.

run = fabric.run_agent(agentflow_id="af_1", goal="summarize last week's refunds")
flow = fabric.create_process_flow(name="nightly-reconcile")
run = fabric.run_process_flow(
    process_flow_id=flow["process_flow_id"], goal="reconcile refunds",
)
status = fabric.get_execution(execution_id=...)   # id from run, see below

create_process_flow returns {"process_flow_id": ..., "flow": {...}}. run_process_flow passes the data-service response through unchanged, so read the execution id off run rather than assuming a field name.

run_agent and run_process_flow both need fabric:agent.run — including run_process_flow, which does not accept fabric:orchestrate.write in its place.

Search code

hits = fabric.search_code(query="def issue_refund", limit=20)

# fetch_code needs either a line span or a row_pk from a search hit
body = fabric.fetch_code(path="billing/refunds.py", start_line=1, end_line=80)

path alone fails validation: without row_pk you must give positive start_line and end_line with end_line >= start_line. search_code also takes path_prefix and lang filters.

These two return their body unwrapped, so result.lifecycle_id is None for them and only for them. Needs fabric:query.read.

Check what you can do, and where you stand on quota

gov = fabric.docs(topic="governance")
gov["mode"]      # "auto" | "manual"
gov["grants"]
gov["quota"]     # project_window_used / _max, org_window_used / _max
gov["rules"]     # the live governance rules, in words

docs is the only place the gateway exposes live quota. Reading it up front lets you pace writes against the real cap instead of discovering it as a QuotaExceeded. Needs fabric:docs.read.


Approvals

This is the part of the SDK worth reading twice.

Some calls do not return a result. They return a pending approval, because a human has to look at them first. The SDK raises ApprovalRequired carrying everything you need to finish the call later.

What gates

Gating is driven by the data you touch, not by the size or kind of the operation. There is no amount threshold. A call gates when:

  • any graph label or argument it touches contains pii, sensitive, confidential or personal — substring match, case-insensitive; or
  • the asset it touches is classified restricted in OpenMetadata.

So save_context with a node label of Refund runs straight through, and the same call with a label of CustomerPersonalData comes back as an approval. On an auto-mode project this gates everyone, admins included.

The second gate site is narrow: a non-admin create_ontology(mode="commit"), and only on an Advanced/manual project.

Writes are not gated for being writes. Reads are not exempt for being reads — a query_context whose Cypher mentions a Personal label gates too.

Because the trigger is the data, handle ApprovalRequired on every governed call, not on the one step you expect to gate. Classification is populated at runtime, so a call that sailed through yesterday can start gating today with no change on your side.

The complete round trip

Three actors, and they are genuinely three: your code requests, an admin decides, your code resumes. The credential that requested is the only one that can resume.

from wexa import Fabric, ApprovalRequired

fabric = Fabric()

# 1. The call gates. HTTP 202, no result — an ApprovalRequired instead.
try:
    fabric.save_context(nodes=[
        {"label": "CustomerPersonalData", "key": "id",
         "properties": {"id": "cust_8841", "email": "ada@example.com"}},
    ])
except ApprovalRequired as e:
    print(e.approval_id)     # apr_000012
    print(e.resume_token)    # rtok_000012_17...   single use, keep it safe
    print(e.lifecycle_id)    # the lifecycle that parked
    token = e.resume_token
# 2. A human decides. The deciding credential must hold an admin role —
#    OWNER / ORG_ADMIN / PROJECT_ADMIN. Usually it happens in the console.
admin = Fabric(api_key="fab_sk_admin_...")
[a for a in admin.approvals("pending")]
# [{'id': 'apr_000012', 'tool': 'save-context', 'what': '...',
#   'requested_by': 'usr_311', 'status': 'pending', ...}]
admin.approve("apr_000012")

A non-admin credential calling approve() gets ForbiddenError: approver must be an admin role (HTTP 403). The role check is the whole check — the gateway records decided_by but does not compare it to requested_by, so an admin approving their own request is not blocked by the API. If your policy needs that, enforce it on your side.

# 3. Your code resumes. Send ONLY the token — the engine substitutes the
#    approved arguments, so anything else you pass is ignored.
result = fabric.save_context(resume_token=token)
print(result)                # the write landed
print(result.lifecycle_id)

The resume must come from the same user and project that requested it. An admin cannot resume on your behalf, and the approved arguments win — you cannot amend a call at approval time.

Approvals expire. The default TTL is 24 hours; after that the request goes to expired and the token is dead.

Blocking instead of handling it yourself

If your process can afford to sit and wait, opt in and the client polls for you:

result = fabric.save_context(
    nodes=[{"label": "CustomerPersonalData", "key": "id", "properties": {...}}],
    wait_for_approval=True,
    approval_timeout=600,     # seconds; raises ApprovalError past this
    poll=5,                   # seconds between checks
)

This does the whole round trip: gate, poll approvals(), resume with the token. A rejection or expiry raises ApprovalError.

Resuming from a durable pipeline

wait_for_approval=True blocks in-process, so a crash or a deploy while a human is deliberating loses an approval that was actually granted. If that matters, checkpoint the token before you block, and resume later — possibly from another process:

try:
    fabric.save_context(nodes=[...])
except ApprovalRequired as e:
    checkpoint(e.approval_id, e.resume_token)     # durable, before you wait

# ... minutes or hours later, a different process ...
fabric.save_context(resume_token=load_token(), retry=True)

Passing resume_token= takes exactly the same code path as wait_for_approval, so everything below behaves identically either way.

A failed resume is not always safe to retry

The token is single-use, and the gateway redeems it partway through the lifecycle:

S2 scope → S3 quota → REDEEM TOKEN → S4 validate → S5 policy → S8 execute

A refusal before redemption leaves the token intact. A refusal after redemption has burned it: the write did not land, and re-running needs a new human approval. Every exception carries resume_spent to tell the two apart.

from wexa import WexaError

try:
    fabric.save_context(resume_token=token, retry=True)
except WexaError as e:
    if e.resume_spent:
        alert_human(e.lifecycle_id)   # approval consumed, write did NOT land
    else:
        pass                          # nothing consumed, safe to re-run as-is
Refusal on resume Stage resume_spent What to do
QuotaExceeded (429) S3, before redemption False Re-send the same token
ValidationError (400/422) S4, after redemption True Needs a new approval
PolicyDenied (403) S5, after redemption True Needs a new approval
UpstreamError (502) S8, after redemption True Needs a new approval

retry=True covers the resume, but only for QuotaExceeded — the one stage that refuses before redemption. Retrying anything later would re-send a spent token and earn a misleading ApprovalError: resume rejected: approval not found, burying the real error. Replaying a token you already spent gets you that same message.


Reference

Methods

Area Methods Grant
Graph read query_context fabric:query.read
Graph write save_context, create_ontology fabric:ontology.write
Code search_code, fetch_code fabric:query.read
Docs docs fabric:docs.read
Connectors connector_read fabric:query.read
Execution run_agent, run_process_flow fabric:agent.run
Orchestrate read get_process_flow, get_execution, list_skills, knowledge_base_retrieve fabric:orchestrate.read
Orchestrate write create_process_flow, update_process_flow fabric:orchestrate.write
Governance whoami, approvals, approve, reject, lifecycle

Two more grants exist for planes this client does not wrap: fabric:catalog.read / fabric:catalog.write (catalog endpoints) and fabric:codesync.write (the /v1/codesync/* ingest plane).

Grants live in the token's scope claim. Missing one fails at S2 with the exact string you need:

ForbiddenError: token missing required grant "fabric:ontology.write"

fabric.docs(topic="governance")["grants"] lists what you actually hold.

call() reaches any tool by name and accepts the gateway's own hyphenated spelling, so these three are the same call:

fabric.run_process_flow(process_flow_id="pf_1", goal="reconcile")
fabric.call("run_process_flow", process_flow_id="pf_1", goal="reconcile")
fabric.call("run-process-flow", process_flow_id="pf_1", goal="reconcile")

Errors

Every failure is typed by the stage that refused it and carries trace_id, and usually lifecycle_id. Quote both in a support request.

e.stage is set only when the gateway returned an S… code — S2:resolve-scope through S8:execute. Refusals that happen outside the lifecycle (401, the forbidden 403 from approve(), 404, 409, unconfigured) carry stage=None, and a 401 predates the lifecycle entirely so there is no lifecycle_id to quote either. The stage column below is the conceptual stage, not a promise about e.stage.

from wexa import PolicyDenied, ForbiddenError, ValidationError, QuotaExceeded

try:
    fabric.run_agent(agentflow_id="af_1", goal="summarize last week")
except PolicyDenied as e:        # S5 — a policy rule refused it
    print(e.stage, e.lifecycle_id)
except ForbiddenError:           # S2 — the token lacks the grant
    ...
except ValidationError:          # S4/S7 — the request is malformed
    ...
except QuotaExceeded:            # S3 — over the window
    ...
Exception HTTP Stage Means Do
AuthError 401 S1 invalid_token, unauthorized — key wrong, revoked or expired Refresh the key, then one retry
ForbiddenError 403 S2 Token lacks the grant, or scope is invalid Mint a key with the named grant. Terminal
PolicyDenied 403 S5 A policy rule refused the call outright Terminal. Change what you're asking for
ValidationError 400/422 S4, S7 Malformed body, bad Cypher, failed dry-run Fix the payload. Terminal
ApprovalRequired 202 S6 A human must decide first See Approvals
ApprovalError 403 S6 Resume token bad, expired, rejected or already spent Needs a new approval
QuotaExceeded 429 S3 Over 120/min project or 600/min org Back off and retry
NotFound 404 No such approval, flow or execution in your org Terminal
ConflictError 409 Approval not pending, agent not ready Re-read state, then decide
UpstreamError 502 S8 A downstream service refused or was unreachable Retry with backoff
TimeoutError_ 504 S8 Upstream deadline exceeded Retry with backoff
ConfigurationError 502/503 unconfigured, unavailable — a deployment problem, not load Retrying will not help. Tell an operator

Only UpstreamError (so TimeoutError_ too) and QuotaExceeded are retried by retry=True. Everything else refuses identically forever.

Three inheritance traps, because except order decides what you catch:

  • PolicyDenied is a ForbiddenError. Catch PolicyDenied first or a policy denial looks like a missing grant.
  • TimeoutError_ is an UpstreamError, and it is not Python's builtin TimeoutError — hence the underscore.
  • ApprovalRequired is a WexaError. In a shared helper, catch it before except WexaError or pending approvals get logged as failures.

WexaError only covers responses the gateway actually sent. An unreachable workspace, DNS that does not resolve or a TLS failure never reaches the gateway and surfaces as urllib's own OSError (URLError is a subclass). A top-level handler needs both:

try:
    fabric.query_context(query=...)
except WexaError:      # the gateway refused
    ...
except OSError:        # never got there
    ...

Results

A tool call returns a dict subclass with the correlation ids attached rather than nested, so you can treat it as the payload and still trace it. The governance methods (whoami, approvals, approve, reject, lifecycle) return plain dicts and lists — no .lifecycle_id on those.

r = fabric.docs(topic="governance")
r["mode"]
r.lifecycle_id      # None for search_code and fetch_code only
r.trace_id

The client sends a W3C traceparent on every request and reads X-Trace-Id back, so correlation works even where telemetry is switched off.

fabric.lifecycle(lifecycle_id) returns the full per-stage record for a call: what each of the ten stages saw and decided.

Retries

Nothing retries unless you ask. The gateway has no idempotency key, so a retried run_agent starts a second real run.

fabric.query_context(query=..., retry=True)     # safe: it's a read

retry=True uses full jitter, capped at the 60-second quota window, because tool routes send no Retry-After and no X-RateLimit-* headers and the limiter is per-pod — the client cannot know the real ceiling. Attempt count comes from the constructor.

Constructor

Fabric(
    workspace=None,   # or WEXA_WORKSPACE
    api_key=None,     # or WEXA_API_KEY. Bearer JWT or API key
    timeout=70,       # seconds. Governed tool routes cap near 60s upstream
    retries=3,        # attempts when retry=True; ignored otherwise
)

Missing workspace or key raises ConfigurationError immediately, before any network call.

The ten stages

Useful when you are reading an error, not before.

Stage Name Refuses with
S1 authenticate AuthError
S2 resolve-scope ForbiddenError
S3 rate-quota QuotaExceeded
S4 validate-input ValidationError
S5 policy PolicyDenied
S6 approval ApprovalRequired, ApprovalError
S7 dry-run ValidationError
S8 execute UpstreamError, TimeoutError_
S9 post-process
S10 record

Two things about S7 that surprise people. It runs after approval, not before — so a dry-run failure on a gated call has already spent the token. And it produces a one-line human-readable summary string, not a structured before/after diff. There is no client-side dry-run switch; the stage is server-driven, and its output shows up in the lifecycle record.

Quota (S3) is a fixed 60-second window: 120 requests per minute per project, 600 per minute per org.


Edge cases

Arguments the server owns

The gateway binds these from your token scope, so the client refuses them before building the request:

project_id   projectID   organization_id   executed_by

query_context rejects a mismatching project_id outright; the others were silently overwritten, which is worse — your value accepted and ignored. Both become one clear client-side ValidationError instead.

Unknown argument keys are dropped silently

A misspelled key is a 200 that wrote nothing, not an error. The two shapes worth double-checking:

# create_ontology — domain is required; it's nodes/relationships, not entities
fabric.create_ontology(
    domain="risk",
    nodes=[{"name": "Vendor", "label": "Vendor", "primaryKey": "vendor_id"}],
    relationships=[{"name": "uses", "fromNode": "Vendor", "toNode": "Service"}],
)

# save_context — relationship endpoints are {label, key, value} objects
fabric.save_context(
    nodes=[{"label": "Vendor", "key": "vendor_id", "properties": {"vendor_id": "v1"}}],
    relationships=[{"type": "USES",
                    "from": {"label": "Vendor", "key": "vendor_id", "value": "v1"},
                    "to": {"label": "Service", "key": "svc_id", "value": "s1"}}],
)

Project mode decides your ingestion path

A project is in Simple/auto or Advanced/manual mode, and the mode — not your arguments — decides how ingestion works. The two halves are mutually exclusive:

Simple / auto Advanced / manual
save_context works fails validation outright
create_ontology(mode="commit") applies, never gated gated for a non-admin
PII/sensitive gate gates everyone, admins included admins bypass, others gated

Check the mode before you write anything. A pipeline that commits an ontology under human approval and then calls save_context will spend a real person's approval on a run that cannot finish.

Note the diagonal: on the only mode where save_context works, the ontology gate cannot fire — so the data-classification rule is the sole gate in play, and it attaches to calls by their data, not by their name. That is the concrete reason to route every governed call through one shared helper that handles ApprovalRequired.

fabric.docs(topic="governance")["mode"]     # "auto" | "manual"

Why isn't my call gating?

On a bare dev stack, usually because no gate is armed. The classification rule needs OpenMetadata configuredOM_SYNC_ENABLED=true plus a host and a project id. The token is not checked when arming, and the host need not be reachable: the bridge is installed before any sync runs, and the label check is a plain substring test that needs no synced data, so a dead OM host still gates. With no OM configured at all, the rule is inert. Combined with the ontology gate being short-circuited in auto mode, that leaves no gate able to fire and ApprovalRequired unreachable.

To arm it against a host that does not exist:

OM_SYNC_ENABLED=true OM_HOST=http://127.0.0.1:1 OM_SYNC_PROJECT_ID=…

That is a deployment state, not a bug in your code, and no code change on your side will alter it.

Not covered yet

  • OAuth 2.1. API keys and Bearer JWTs only for now.
  • create-agent. The handler exists in the gateway but is not registered, so it is reachable over neither REST nor MCP. Build agents into a process flow manifest instead.
  • Chat completions. /v1/agents/{id}/chat/completions is OpenAI-shaped — point the openai package at your API base. It has its own per-key rate limit that does send Retry-After, does not support streaming, and can block for several minutes.
  • Catalog and code-sync endpoints, an async client, pagination helpers.

What has been verified against a running gateway

Confirmed live — SDK method → route → auth → lifecycle → handler. Exercised on the tools named below, not on all fifteen: eight of them (list_skills, the four *_process_flow methods, get_execution, knowledge_base_retrieve, connector_read) have no REST route on dev yet, and REST is this client's only transport — so they cannot have been confirmed through it. They are routed by the pr/gateway-orchestrate-rest change; until that lands, treat them as unproven.

  • whoami and docs return real scope, grant, mode and quota payloads.
  • The full approval round trip. A save_context with an ordinary node label runs straight through; one with a CustomerPersonalData label returns 202 with {approval_id, resume_token, lifecycle_id}; the request appears in approvals(); approve() succeeds from an admin credential; the resume with only the token lands the write.
  • Approving is gated on role, not on being a different person: a non-admin credential gets forbidden / "approver must be an admin role". This is not separation of duties — an admin approving her own request was verified to succeed (requested_by == decided_by, status approved), because approval.decide() records the decider and never compares it to the requester. Enforce requester ≠ approver in your own code if you need it.
  • Resume tokens are single-use. A resume that fails after redemption reports resume_spent = True, and replaying that token yields ApprovalError: resume rejected: approval not found.
  • Typed error mapping, including ValidationError carrying a real lifecycle_id, and the S2 missing-grant message.

Still source-derived only:

  • create_ontology(mode="commit") gating for a non-admin on a manual project.
  • The 429 boundary in practice. The limits themselves are configuration: 120/min project, 600/min org, fixed 60s window.
  • The response bodies of run_process_flow and get_execution. The argument shapes above come from the handlers' input schemas, not from an observed round trip.

Development

python3 wexa.py                     # offline self-check of the approval flow
WEXA_WORKSPACE=... WEXA_API_KEY=... python3 examples/status.py

wexa.py's demo() covers the branches that actually have logic: the 202 → approve → resume round trip, the resume retry boundary, tool routing, and the server-bound argument guard.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wexa-0.1.0.tar.gz (39.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wexa-0.1.0-py3-none-any.whl (18.4 kB view details)

Uploaded Python 3

File details

Details for the file wexa-0.1.0.tar.gz.

File metadata

  • Download URL: wexa-0.1.0.tar.gz
  • Upload date:
  • Size: 39.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for wexa-0.1.0.tar.gz
Algorithm Hash digest
SHA256 dcf7028dcac7c84d577747d183702c7bec29b2ae3884c885bf6e2923a7bd670e
MD5 a339dfca5b557d9d89e840d659931253
BLAKE2b-256 973122235adb985a86e7c4684eb6f5d71da22353870a3c0941ed3fa63acd462b

See more details on using hashes here.

File details

Details for the file wexa-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: wexa-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for wexa-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 86b878174464456da83e37b01315da9c31b7be3e9166d1492225392ae2a27477
MD5 1e2724ed91d50bd8d0dda78f1450bd54
BLAKE2b-256 d7cd57fbb2d5f5a9dcb4a84323a323e3b605abce7ac94ce5755b4e6376cce423

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page