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
pip install -e . # from a clone, for development
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:
- 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.
- 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,confidentialorpersonal— 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:
PolicyDeniedis aForbiddenError. CatchPolicyDeniedfirst or a policy denial looks like a missing grant.TimeoutError_is anUpstreamError, and it is not Python's builtinTimeoutError— hence the underscore.ApprovalRequiredis aWexaError. In a shared helper, catch it beforeexcept WexaErroror 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 configured — OM_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/completionsis OpenAI-shaped — point theopenaipackage at your API base. It has its own per-key rate limit that does sendRetry-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
All fifteen tools now have a REST route — pr/gateway-orchestrate-rest
landed, so the earlier eight-tool gap is closed and every method on this client
is reachable.
Routed is not the same as exercised. The confirmations below cover the tools
named in them; eight methods (list_skills, the four *_process_flow methods,
get_execution, knowledge_base_retrieve, connector_read) are routed but
have not yet been driven end-to-end over REST, so treat their response shapes
as unconfirmed.
whoamianddocsreturn real scope, grant, mode and quota payloads.- The full approval round trip. A
save_contextwith an ordinary node label runs straight through; one with aCustomerPersonalDatalabel returns 202 with{approval_id, resume_token, lifecycle_id}; the request appears inapprovals();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, statusapproved), becauseapproval.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 yieldsApprovalError: resume rejected: approval not found. - Typed error mapping, including
ValidationErrorcarrying a reallifecycle_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_flowandget_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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wexa-0.1.1.tar.gz.
File metadata
- Download URL: wexa-0.1.1.tar.gz
- Upload date:
- Size: 39.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0176e411166ed5ad444580df9525819d6acbdff3ae3bee37ad00dae8d40e4ec8
|
|
| MD5 |
55aefc5b42c0eec47e3dfe5961e8a128
|
|
| BLAKE2b-256 |
06b49a22b5d0dbe03672ba570b15b7dc1fb3e6be7d37093a36137c72e686a6cf
|
File details
Details for the file wexa-0.1.1-py3-none-any.whl.
File metadata
- Download URL: wexa-0.1.1-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
007aa92e08d3c993ba760cd8814bea4576b4fa7944dee9cc335a61071df5fe14
|
|
| MD5 |
b4f90aba8e225194c3dff367494be037
|
|
| BLAKE2b-256 |
5c411a6c7e8a72a134a1eb5bd50ed3892f2d39f88b37794c82f03a61949f5f98
|