Skip to content

feat: client.requests + client.inbox — approve/reject both request systems programmatically (BLDX-1611) - #1003

Open
Aryamanz29 wants to merge 12 commits into
mainfrom
aryaman/bldx-1611
Open

feat: client.requests + client.inbox — approve/reject both request systems programmatically (BLDX-1611)#1003
Aryamanz29 wants to merge 12 commits into
mainfrom
aryaman/bldx-1611

Conversation

@Aryamanz29

@Aryamanz29 Aryamanz29 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Problem

Customers cannot manage Atlan's request/approval systems programmatically (Zendesk 127299 / BLDX-1611). Atlan has two coexisting systems and pyatlan covered neither:

Classic Requests module Governance Workflows Inbox
API Heracles /requests Heracles /approval-workflow-requests
UI Governance Center → Requests Inbox (Labs: GOVERNANCE_WORKFLOWS_INBOX, off by default)
atlan-java parity has it (RequestsEndpoint) neither SDK has it

What this adds

client.requests (classic, java parity + improvements):

client.requests.list(status=AtlanRequestStatus.ACTIVE)      # typed filters, lazy pagination
client.requests.create(AttributeRequest.creator(...))
client.requests.approve(guid, msg) / .reject(guid, msg)      # applies / discards the change

client.inbox (governance workflows, first SDK anywhere):

client.inbox.get(wf_request_guid)                            # approver routing, status
client.inbox.approve_all(group_key, sub_type?, comment?)     # native server-side bulk
client.inbox.reject_all(...)
# group_key = a task guid (one task) or an asset guid (ALL pending tasks on it)
# inbox tasks are Task assets — list via FluentSearch(Task)

Sync + async parity throughout; typed filters ($in grammar mirrored from the UI — plain equality is silently ignored by the endpoint); enums (AtlanRequestStatus, AtlanRequestType, ApprovalWorkflowRequestType); lazy pagination matching UserResponse/GroupResponse.

Live validation (all cells verified on real tenants)

Classic (self-contained integration test, 3/3 passing live): create → typed-filter list → get → approve → change lands on the backend entity → reject → change NOT applied.

Inbox (validated on a workflows-enabled tenant):

  • task discovery incl. recipients/states/workflow-request guids ✅
  • get() with the workflow-request guid (found in the task's task_actions fulfillment URL; the task guid itself 500s) ✅
  • reject_all(task_guid)total_tasks: 1 ✅ · approve_all(task_guid)total_tasks: 1, task flips to APPROVED ✅
  • approve_all(asset_guid)total_tasks: 2, both tasks flip to APPROVED ✅ (true bulk)
  • controlled negative: same call by a non-recipient → 1003 ✅

The identity rule (the actual customer blocker)

Approval rights follow the token's identity, in both systems:

  • Classic: the service account must be an eligible approver — an admin-role key works (validated).
  • Inbox: strictly recipient-scoped; an admin role does NOT override it (validated with a controlled experiment). The workflow builder currently supports only human users/groups as approvers, so headless automation may require a user token — flagged as a product gap (follow-up ticket).

The server's misleading 1003: No pending tasks found is translated by the SDK into an actionable message naming both real causes (already-actioned vs foreign recipient) — each observed live.

Wire-truth fixes found during live testing

  • filter grammar: status requires $in + isDuplicate clause (plain equality silently ignored)
  • approvedBy/rejectedBy: string OR list-of-objects depending on version → lenient + regression-tested
  • approval_details: object, not list → lenient
  • by-id endpoints: return single-element lists (unwrapped); non-request guids 500; malformed guids 400 on a format regex

Testing

  • 18 unit tests (wire shapes, filter grammar, pagination laziness, enum coercion, 1003 translation, shape variants)
  • Integration: atlan_requests_test.py (self-contained, passing live) + inbox_test.py (env-gated: workflow tasks cannot be self-created — workflow_guids required — so gated on pre-seeded task/asset guids)
  • ./qa-checks clean (1,013 files), full unit suite 6,877 green

Closes BLDX-1611.

🤖 Generated with Claude Code

…ox requests (BLDX-1611)

Ports the atlan-java RequestsEndpoint surface to pyatlan. Customers could
not approve Metadata Inbox requests programmatically: the Heracles REST
endpoints exist but pyatlan had no client for them (hasattr(client,
'requests') was False on every release).

Adds:
- model/atlan_request.py: AtlanRequest, AttributeRequest (creator),
  AtlanRequestAction, AtlanRequestResponse
- client.requests (sync) and AsyncAtlanClient.requests (async) with
  list / list_actionable / get / create / approve / reject
- Heracles API constants for /requests, /requests/actionable,
  /requests/{id}, /requests/{id}/action

Notes:
- AttributeRequest.creator() sets requestType/approvalType/sourceType
  explicitly: Heracles requires them on the wire and pyatlan's
  exclude_unset serialization strips declared defaults (the BLDX-1589
  trap) — test-covered.
- GET /requests/{id} can return a single-element list; the client
  unwraps it (observed live).
- Visibility caveat documented on both clients: requests are scoped to
  the token's identity — the API key's service account must be an admin
  (or designated approver) to see and action requests raised for humans.

Validated end-to-end against a live tenant with an admin service-account
key: create -> list (filtered) -> get -> list_actionable -> approve ->
attribute change APPLIED on the asset; reject -> change NOT applied.
Unit: 8 new tests, full suite 6,867 green, qa-checks clean.
@linear

linear Bot commented Aug 7, 2026

Copy link
Copy Markdown

BLDX-1611

…ct) alongside classic client.requests (BLDX-1611)

Tenants can run two coexisting request systems: the classic Requests
module (client.requests, Heracles /requests) and the newer Governance
Workflows Inbox (Labs flag GOVERNANCE_WORKFLOWS_INBOX; Heracles
/approval-workflow-requests). Customers straddle both, so the SDK now
speaks both:

- client.inbox.get(guid) — one approval-workflow request
- client.inbox.approve_all(group_key, sub_type?, comment?) — native
  server-side bulk approval (PUT /actions/bulk); group_key is the task
  GUID or related asset GUID
- client.inbox.reject_all(...) — bulk rejection
- Inbox tasks are Task assets — list them with FluentSearch on Task

Notes:
- this API family is snake_case on the wire (unlike classic camelCase);
  models disable the camelCase alias generator and a test guards it
- sync + async parity; 5 new unit tests
…Is (BLDX-1611)

- client.requests.list()/list_actionable() take typed, discoverable
  filters (status, request_type, destination_guid/qualified_name,
  entity_type, created_by) instead of requiring callers to know the
  Heracles JSON filter grammar; raw post_filter stays as an escape hatch
  and refuses to be silently combined with typed filters
- new enums: AtlanRequestStatus, AtlanRequestType (all wire values from
  the Heracles contract), ApprovalWorkflowRequestType (Inbox task
  sub-types) — used across both clients
- AtlanRequestResponse / AsyncAtlanRequestResponse now paginate lazily
  (iterate to walk ALL matches), mirroring UserResponse/GroupResponse
- 5 new unit tests: typed-filter wire shape, AND-combining, raw+typed
  rejection, lazy pagination call pattern, enum sub_type coercion
@Aryamanz29 Aryamanz29 changed the title feat: client.requests — approve/reject Metadata Inbox requests programmatically (BLDX-1611) feat: client.requests + client.inbox — approve/reject both request systems programmatically (BLDX-1611) Aug 7, 2026
…ulk-action (BLDX-1611)

Classic flow is fully self-contained: creates its own AttributeRequest
against a disposable term, finds it via the typed filter, approves and
verifies the change LANDED on the backend entity, rejects a second and
verifies it did not. Passes live (3 passed) against a test tenant.

The inbox bulk-action test is gated on ATLAN_TEST_WORKFLOW_GROUP_KEY:
creating workflow tasks programmatically requires an existing governance
workflow (workflow_guids is required on POST /approval-workflow-requests),
which only tenants with the Governance Workflows Labs feature have.
…cases (BLDX-1611)

atlan_requests_test.py now covers only the classic Requests module
(self-contained). inbox_test.py covers client.inbox with the two cases
that pin the bulk semantics:

- group_key = task guid  -> exactly ONE task actioned (group of one)
- group_key = asset guid -> EVERY pending task on the asset actioned
  (requires 2+ seeded tasks; asserts total_tasks >= 2)

Both env-gated (ATLAN_TEST_INBOX_TASK_GUID / ATLAN_TEST_INBOX_ASSET_GUID)
because workflow tasks cannot be self-created — raising one requires a
configured governance workflow on the tenant.
…ate exclusion (BLDX-1611)

Live testing on a workflows-enabled tenant showed plain equality filters
({"status": "active"}) are silently ignored by the endpoint — a list
filtered to active returned already-approved and rejected requests too.
The Atlan UI's own grammar works: status through the $in operator, AND'd
with an isDuplicate:false clause. build_requests_filter now emits exactly
that shape (request_type gets $in as well; plain equality kept for guid/
qualified-name/entity-type/created-by, which match verified live).
…uests (BLDX-1611)

Live kill-argo testing: listing a page containing already-actioned
requests crashed with a pydantic ValidationError — the server returns
rejectedBy/approvedBy as arrays (multi-approver), while the model (copied
from atlan-java, which is stale here) typed them str. Now
Union[str, List[str]] with a regression test parsing an actioned record.
…latform version (BLDX-1611)

Second live failure on the same fields: rejectedBy is a list of approver
OBJECTS on kill-argo (first fix assumed list of strings — typed from
inference, not evidence). These fields now parse as Any with the variants
documented; regression test parses string, string-list, and object-list
shapes. Follow-up: capture the real payload and introduce a typed
Approver model from evidence.
…ire, not a list (BLDX-1611)

Live get() on a workflows-enabled tenant returns approval_details as an
OBJECT ({is_auto_approved, manual_approver_details:{approvers, strategy}}),
not a list. Kept untyped like the other version-variant fields.

Live validation captured: get() requires the WORKFLOW-REQUEST guid (found
in the Task asset's task_actions fulfillment URL — the task guid itself
500s), and bulk actions are strictly recipient-scoped: neither task guid,
asset guid, nor request guid actions a task whose recipient is another
user, even for a gov-admin caller.
…xplanation (BLDX-1611)

Validated live with a controlled experiment (same caller, two tasks):
- task addressed to the caller -> bulk reject queued, total_tasks=1
- task addressed to another user -> 1003 'No pending tasks found for the
  specified group', even though the group visibly has a pending task

Bulk approvals are strictly recipient-scoped and an admin role does not
override it. The server message reads like a wrong group key; the SDK now
raises an actionable message explaining the scoping and the automation
path (workflow approver config currently supports only human users and
groups, so automation may require a user token).
…foreign recipient (BLDX-1611)

Live testing surfaced the second cause: a group whose tasks were all
already approved returns the same 1003, and the previous message wrongly
asserted recipient mismatch. The message now names both causes and how to
distinguish them (task_execution_action via Task search).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant