Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions introspection_sdk/resources/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@
ConnectionTokenResult,
Connector,
ConnectorApp,
ConnectorApprovalPolicy,
ConnectorAuthMode,
ConnectorAuthorization,
ConnectorAuthorizeBinding,
ConnectorAuthorizeRequest,
ConnectorCreateRequest,
ConnectorOAuthDiscoveryRequest,
ConnectorOAuthDiscoveryResponse,
ConnectorPersonServerMode,
ConnectorStatus,
ConnectorUpdateRequest,
PolicyEntity,
)
from introspection_sdk.schemas.pagination import Paginated
from introspection_sdk.schemas.runner import RunnerIdentity
Expand All @@ -70,11 +69,10 @@ def _create_body(
signing_secret: str | None,
metadata: dict[str, Any] | None,
issuer: str | None,
person_server_mode: ConnectorPersonServerMode | str | None,
person_server_url: str | None,
approval_policy: ConnectorApprovalPolicy | str | None,
application_id: UUID | None,
assertion_audience: str | None,
policy_entities: list[PolicyEntity | dict[str, Any]] | None,
webhook_url: str | None,
) -> dict[str, Any]:
# Loose public inputs (plain str) are coerced by validation:
Expand All @@ -97,11 +95,10 @@ def _create_body(
"signing_secret": signing_secret,
"metadata": metadata,
"issuer": issuer,
"person_server_mode": person_server_mode,
"person_server_url": person_server_url,
"approval_policy": approval_policy,
"application_id": application_id,
"assertion_audience": assertion_audience,
"policy_entities": policy_entities,
"webhook_url": webhook_url,
}
).model_dump(mode="json", exclude_none=True)
Expand All @@ -116,6 +113,7 @@ def _update_body(
status: ConnectorStatus | str | None,
metadata: dict[str, Any] | None,
webhook_url: str | None,
policy_entities: list[PolicyEntity | dict[str, Any]] | None,
client_secret: str | None,
signing_secret: str | None,
) -> dict[str, Any]:
Expand All @@ -130,6 +128,7 @@ def _update_body(
"status": status,
"metadata": metadata,
"webhook_url": webhook_url,
"policy_entities": policy_entities,
"client_secret": client_secret,
"signing_secret": signing_secret,
}
Expand Down Expand Up @@ -359,11 +358,11 @@ def create(
signing_secret: str | None = None,
metadata: dict[str, Any] | None = None,
issuer: str | None = None,
person_server_mode: ConnectorPersonServerMode | str | None = None,
person_server_url: str | None = None,
approval_policy: ConnectorApprovalPolicy | str | None = None,
application_id: UUID | None = None,
assertion_audience: str | None = None,
policy_entities: builtins.list[PolicyEntity | dict[str, Any]]
| None = None,
webhook_url: str | None = None,
) -> Connector:
"""Create a connector, or replace one with the same ``slug``.
Expand All @@ -379,7 +378,10 @@ def create(
the OAuth endpoints from ``.well-known`` discovery and, where the
provider supports it, register an OAuth client on its own. A
``pipedream`` connector requires ``metadata["provider_workspace_id"]``
— its Pipedream Connect project id (``proj_...``).
— its Pipedream Connect project id (``proj_...``). An ``aauth``
connector requires ``person_server_url`` and ``application_id``;
``policy_entities`` are the Cedar entities the egress policy gate
adds to its requests.
"""
payload = self._http.request(
"POST",
Expand All @@ -400,11 +402,10 @@ def create(
signing_secret=signing_secret,
metadata=metadata,
issuer=issuer,
person_server_mode=person_server_mode,
person_server_url=person_server_url,
approval_policy=approval_policy,
application_id=application_id,
assertion_audience=assertion_audience,
policy_entities=policy_entities,
webhook_url=webhook_url,
),
)
Expand Down Expand Up @@ -486,12 +487,15 @@ def update(
status: ConnectorStatus | str | None = None,
metadata: dict[str, Any] | None = None,
webhook_url: str | None = None,
policy_entities: builtins.list[PolicyEntity | dict[str, Any]]
| None = None,
client_secret: str | None = None,
signing_secret: str | None = None,
) -> Connector:
"""Update a connector. Only provided fields change; an omitted
``client_secret`` / ``signing_secret`` stays as it is (write-only
rotation, never a clear)."""
rotation, never a clear). ``policy_entities`` replaces the
connector's Cedar entities."""
payload = self._http.request(
"PATCH",
f"/v1/connectors/{connector_id}",
Expand All @@ -503,6 +507,7 @@ def update(
status=status,
metadata=metadata,
webhook_url=webhook_url,
policy_entities=policy_entities,
client_secret=client_secret,
signing_secret=signing_secret,
),
Expand Down Expand Up @@ -715,11 +720,11 @@ async def create(
signing_secret: str | None = None,
metadata: dict[str, Any] | None = None,
issuer: str | None = None,
person_server_mode: ConnectorPersonServerMode | str | None = None,
person_server_url: str | None = None,
approval_policy: ConnectorApprovalPolicy | str | None = None,
application_id: UUID | None = None,
assertion_audience: str | None = None,
policy_entities: builtins.list[PolicyEntity | dict[str, Any]]
| None = None,
webhook_url: str | None = None,
) -> Connector:
"""Create a connector, or replace one with the same ``slug``.
Expand All @@ -735,7 +740,10 @@ async def create(
the OAuth endpoints from ``.well-known`` discovery and, where the
provider supports it, register an OAuth client on its own. A
``pipedream`` connector requires ``metadata["provider_workspace_id"]``
— its Pipedream Connect project id (``proj_...``).
— its Pipedream Connect project id (``proj_...``). An ``aauth``
connector requires ``person_server_url`` and ``application_id``;
``policy_entities`` are the Cedar entities the egress policy gate
adds to its requests.
"""
payload = await self._http.request(
"POST",
Expand All @@ -756,11 +764,10 @@ async def create(
signing_secret=signing_secret,
metadata=metadata,
issuer=issuer,
person_server_mode=person_server_mode,
person_server_url=person_server_url,
approval_policy=approval_policy,
application_id=application_id,
assertion_audience=assertion_audience,
policy_entities=policy_entities,
webhook_url=webhook_url,
),
)
Expand Down Expand Up @@ -844,12 +851,15 @@ async def update(
status: ConnectorStatus | str | None = None,
metadata: dict[str, Any] | None = None,
webhook_url: str | None = None,
policy_entities: builtins.list[PolicyEntity | dict[str, Any]]
| None = None,
client_secret: str | None = None,
signing_secret: str | None = None,
) -> Connector:
"""Update a connector. Only provided fields change; an omitted
``client_secret`` / ``signing_secret`` stays as it is (write-only
rotation, never a clear)."""
rotation, never a clear). ``policy_entities`` replaces the
connector's Cedar entities."""
payload = await self._http.request(
"PATCH",
f"/v1/connectors/{connector_id}",
Expand All @@ -861,6 +871,7 @@ async def update(
status=status,
metadata=metadata,
webhook_url=webhook_url,
policy_entities=policy_entities,
client_secret=client_secret,
signing_secret=signing_secret,
),
Expand Down
94 changes: 42 additions & 52 deletions introspection_sdk/resources/members.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
Members are the organization's principals: ``business`` humans,
``agent`` members, and ``customer`` members minted from an asserted
identity (:class:`~introspection_sdk.schemas.runner.RunnerIdentity`).
``create`` invites a human by email; ``customer`` members are never
created here, only by identity assertion.
``create`` invites a human by email, or (owner only) records a
``customer`` member by its ``external_user_id``; customer members are
also minted by identity assertion.

Reads need ``members:read``. ``update`` needs ``members:manage`` (owner),
as does seeding ``tags`` on ``create``: a member tag is access-bearing.
``metadata`` grants nothing.
as does seeding ``tags`` or ``policy_attributes`` on ``create``: a member
tag is access-bearing, and policy attributes decide what the member's agents
may do. ``metadata`` grants nothing.
"""

from __future__ import annotations
Expand All @@ -17,6 +19,8 @@
from typing import Any
from uuid import UUID

from pydantic import BaseModel

from introspection_sdk._http import _AsyncHttpClient, _HttpClient
from introspection_sdk.pagination import (
AsyncPager,
Expand All @@ -25,8 +29,10 @@
cursor_paginate,
)
from introspection_sdk.schemas.members import (
MEMBER_CREATE_ADAPTER,
BusinessMemberCreate,
CustomerMemberCreate,
Member,
MemberCreateRequest,
MemberType,
MemberUpdateRequest,
)
Expand Down Expand Up @@ -63,16 +69,13 @@ def _list_params(


def _create_body(
*,
email: str,
name: str,
role: str | None,
tags: builtins.list[str] | None,
metadata: dict[str, str] | None,
member: BusinessMemberCreate | CustomerMemberCreate | dict[str, Any],
) -> dict[str, Any]:
return MemberCreateRequest(
email=email, name=name, role=role, tags=tags, metadata=metadata
).model_dump(mode="json", exclude_none=True)
# `member_type` has a default, so `exclude_none` always keeps it.
validated = MEMBER_CREATE_ADAPTER.validate_python(
member.model_dump() if isinstance(member, BaseModel) else member
)
return validated.model_dump(mode="json", exclude_none=True)


def _update_body(
Expand All @@ -82,6 +85,7 @@ def _update_body(
role: str | None,
tags: builtins.list[str] | None,
metadata: dict[str, str] | None,
policy_attributes: dict[str, Any] | None,
) -> dict[str, Any]:
# `exclude_none` drops an omitted field but keeps an explicit [] / {},
# which is what clears tags / metadata — both replace wholesale.
Expand All @@ -91,6 +95,7 @@ def _update_body(
role=role,
tags=tags,
metadata=metadata,
policy_attributes=policy_attributes,
).model_dump(mode="json", exclude_none=True)


Expand Down Expand Up @@ -150,30 +155,22 @@ def get(

def create(
self,
*,
email: str,
name: str,
role: str | None = None,
tags: builtins.list[str] | None = None,
metadata: dict[str, str] | None = None,
member: BusinessMemberCreate | CustomerMemberCreate | dict[str, Any],
) -> Member:
"""Invite a human member by email (admin or owner only).

``name`` must be a full name (first and last). ``tags`` are
access-bearing, so passing any needs ``members:manage``.
``metadata`` keys are letters, digits, ``_`` and ``-``, values are
non-empty strings, at most 64 entries per write; the server answers 422
otherwise."""
"""Create a member: a :class:`BusinessMemberCreate` invites a person
by email (admin or owner only); a :class:`CustomerMemberCreate`
records one of the organization's end users by ``external_user_id``
(owner only, no ``role``).

A plain dict is validated through the same union, and one without
``member_type`` is a business member. ``member_type`` is always
sent. A business ``name`` must be a full name (first and last).
``tags`` and ``policy_attributes`` decide access, so passing either
needs ``members:manage``. ``metadata`` keys are letters, digits,
``_`` and ``-``, values are non-empty strings, at most 64 entries
per write; the server answers 422 otherwise."""
payload = self._http.request(
"POST",
"/v1/members",
json=_create_body(
email=email,
name=name,
role=role,
tags=tags,
metadata=metadata,
),
"POST", "/v1/members", json=_create_body(member)
)
return Member.model_validate(payload)

Expand All @@ -186,12 +183,15 @@ def update(
role: str | None = None,
tags: builtins.list[str] | None = None,
metadata: dict[str, str] | None = None,
policy_attributes: dict[str, Any] | None = None,
) -> Member:
"""Update a member (requires ``members:manage``).

``tags`` and ``metadata`` each replace wholesale: ``None`` leaves
them untouched, ``[]`` / ``{}`` clears them. To change one metadata
key, read the member, edit its map, and send the whole map back."""
key, read the member, edit its map, and send the whole map back.
``policy_attributes`` replaces the member's Cedar attributes for the
egress policy gate; ``None`` leaves them untouched."""
payload = self._http.request(
"PATCH",
f"/v1/members/{member_id}",
Expand All @@ -201,6 +201,7 @@ def update(
role=role,
tags=tags,
metadata=metadata,
policy_attributes=policy_attributes,
),
)
return Member.model_validate(payload)
Expand Down Expand Up @@ -265,24 +266,11 @@ async def get(

async def create(
self,
*,
email: str,
name: str,
role: str | None = None,
tags: builtins.list[str] | None = None,
metadata: dict[str, str] | None = None,
member: BusinessMemberCreate | CustomerMemberCreate | dict[str, Any],
) -> Member:
"""Async twin of :meth:`Members.create`."""
payload = await self._http.request(
"POST",
"/v1/members",
json=_create_body(
email=email,
name=name,
role=role,
tags=tags,
metadata=metadata,
),
"POST", "/v1/members", json=_create_body(member)
)
return Member.model_validate(payload)

Expand All @@ -295,6 +283,7 @@ async def update(
role: str | None = None,
tags: builtins.list[str] | None = None,
metadata: dict[str, str] | None = None,
policy_attributes: dict[str, Any] | None = None,
) -> Member:
"""Async twin of :meth:`Members.update`."""
payload = await self._http.request(
Expand All @@ -306,6 +295,7 @@ async def update(
role=role,
tags=tags,
metadata=metadata,
policy_attributes=policy_attributes,
),
)
return Member.model_validate(payload)
Loading
Loading