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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ print(resent["message_id"]) # the new message; it links back through resent_fro
| `routes` | `list(project_id)`, `iterate(project_id)`, `create(project_id, …)`, `retrieve`, `update`, `delete`, `verify_inbound_domain` |
| `stats` | `retrieve` |
| `suppressions` | `list`, `iterate`, `create`, `delete` |
| `team` | `retrieve`, `update`, `usage`, `roles` |
| `team` | `retrieve`, `update`, `usage`, `roles`, `retrieve_role`, `create_role`, `update_role`, `delete_role`, `duplicate_role` |
| `team.members` | `list`, `iterate`, `retrieve`, `update_assignment` |
| `team.invites` | `list`, `iterate`, `retrieve`, `create`, `update`, `revoke` |
| `webhooks` | `list`, `iterate`, `create`, `retrieve`, `update`, `delete`, `test`, `regenerate_secret` |
Expand All @@ -308,6 +308,17 @@ invite = lettermint.team.invites.create({
lettermint.team.invites.revoke(invite["id"])
```

`team.create_role()` creates a custom role; it needs a plan with custom roles. The permissions must include the permissions they depend on, and role names are unique. `team.update_role()` replaces a custom role's name, description and permissions, and `team.duplicate_role()` copies a role under a new name (without a `description`, the copy keeps the source role's). Built-in roles cannot be changed or deleted, and the Owner role cannot be duplicated. While a role is still assigned to members, invites or SCIM groups, `team.delete_role()` needs a `replacement_role_id` that receives those assignments:

```python
role = lettermint.team.create_role({
"name": "Support",
"description": "Handles customer questions",
"permissions": ["support:manage"],
})
lettermint.team.delete_role(role["id"], {"replacement_role_id": "member-role-id"})
```

`webhooks.deliveries.retry()` sends a delivery's original payload to the webhook again now. The retry counts as a delivery attempt; if it fails with a retryable error, the automatic retries continue. The API answers `409` (`ConflictError`) when the webhook is disabled, the delivery cannot be retried or a retry is already in progress.

### Query parameters and pagination
Expand Down
88 changes: 88 additions & 0 deletions src/lettermint/_async/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
from .._generated.operations import OPERATIONS
from .._generated.types import (
CreateTeamInviteData,
CreateTeamRoleData,
DeleteSuppressionResponse,
DeleteTeamRoleQuery,
DnsVerificationSuccessResponse,
DomainData,
DomainListData,
DomainMutationResponse,
DuplicateTeamRoleData,
GetDomainQuery,
GetProjectQuery,
GetReportForwardingResponse,
Expand Down Expand Up @@ -78,6 +81,7 @@
TeamInviteData,
TeamMemberData,
TeamMutationResponse,
TeamRoleData,
TeamRoleListResponse,
TeamUsageDetailData,
TestWebhookResponse,
Expand All @@ -88,6 +92,7 @@
UpdateTeamData,
UpdateTeamInviteData,
UpdateTeamMemberAssignmentData,
UpdateTeamRoleData,
UpdateWebhookData,
VerifyReportForwardingRequest,
VerifyReportForwardingResponse,
Expand Down Expand Up @@ -924,6 +929,89 @@ async def roles(self, *, timeout: float | None = None) -> TeamRoleListResponse:
await self._request("GET /team/roles", "team.roles", timeout=timeout),
)

async def retrieve_role(self, role_id: str, *, timeout: float | None = None) -> TeamRoleData:
return cast(
TeamRoleData,
await self._request(
"GET /team/roles/{roleId}",
"team.retrieve_role",
path={"roleId": role_id},
timeout=timeout,
),
)

async def create_role(
self, body: CreateTeamRoleData, *, timeout: float | None = None
) -> TeamRoleData:
"""Creates a custom role.

Needs a plan with custom roles. The permissions must include the permissions they depend
on, and role names are unique within the team.
"""
return cast(
TeamRoleData,
await self._request("POST /team/roles", "team.create_role", body=body, timeout=timeout),
)

async def update_role(
self, role_id: str, body: UpdateTeamRoleData, *, timeout: float | None = None
) -> TeamRoleData:
"""Replaces the name, description and permissions of a custom role.

Built-in roles cannot be changed.
"""
return cast(
TeamRoleData,
await self._request(
"PUT /team/roles/{roleId}",
"team.update_role",
path={"roleId": role_id},
body=body,
timeout=timeout,
),
)

async def delete_role(
self,
role_id: str,
query: DeleteTeamRoleQuery | None = None,
*,
timeout: float | None = None,
) -> MessageResponse:
"""Deletes a custom role. Built-in roles cannot be deleted.

While the role is still assigned to members, invites or SCIM groups, pass
``{"replacement_role_id": ...}``: the role that receives those assignments.
"""
return cast(
MessageResponse,
await self._request(
"DELETE /team/roles/{roleId}",
"team.delete_role",
path={"roleId": role_id},
query=query,
timeout=timeout,
),
)

async def duplicate_role(
self, role_id: str, body: DuplicateTeamRoleData, *, timeout: float | None = None
) -> TeamRoleData:
"""Copies a role under a new name. The Owner role cannot be duplicated.

Without a ``description``, the copy keeps the description of the source role.
"""
return cast(
TeamRoleData,
await self._request(
"POST /team/roles/{roleId}/duplicate",
"team.duplicate_role",
path={"roleId": role_id},
body=body,
timeout=timeout,
),
)


class AsyncWebhookDeliveries(AsyncResource):
"""Delivery attempts of a webhook. Team token."""
Expand Down
88 changes: 88 additions & 0 deletions src/lettermint/_sync/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
from .._generated.operations import OPERATIONS
from .._generated.types import (
CreateTeamInviteData,
CreateTeamRoleData,
DeleteSuppressionResponse,
DeleteTeamRoleQuery,
DnsVerificationSuccessResponse,
DomainData,
DomainListData,
DomainMutationResponse,
DuplicateTeamRoleData,
GetDomainQuery,
GetProjectQuery,
GetReportForwardingResponse,
Expand Down Expand Up @@ -79,6 +82,7 @@
TeamInviteData,
TeamMemberData,
TeamMutationResponse,
TeamRoleData,
TeamRoleListResponse,
TeamUsageDetailData,
TestWebhookResponse,
Expand All @@ -89,6 +93,7 @@
UpdateTeamData,
UpdateTeamInviteData,
UpdateTeamMemberAssignmentData,
UpdateTeamRoleData,
UpdateWebhookData,
VerifyReportForwardingRequest,
VerifyReportForwardingResponse,
Expand Down Expand Up @@ -925,6 +930,89 @@ def roles(self, *, timeout: float | None = None) -> TeamRoleListResponse:
self._request("GET /team/roles", "team.roles", timeout=timeout),
)

def retrieve_role(self, role_id: str, *, timeout: float | None = None) -> TeamRoleData:
return cast(
TeamRoleData,
self._request(
"GET /team/roles/{roleId}",
"team.retrieve_role",
path={"roleId": role_id},
timeout=timeout,
),
)

def create_role(
self, body: CreateTeamRoleData, *, timeout: float | None = None
) -> TeamRoleData:
"""Creates a custom role.

Needs a plan with custom roles. The permissions must include the permissions they depend
on, and role names are unique within the team.
"""
return cast(
TeamRoleData,
self._request("POST /team/roles", "team.create_role", body=body, timeout=timeout),
)

def update_role(
self, role_id: str, body: UpdateTeamRoleData, *, timeout: float | None = None
) -> TeamRoleData:
"""Replaces the name, description and permissions of a custom role.

Built-in roles cannot be changed.
"""
return cast(
TeamRoleData,
self._request(
"PUT /team/roles/{roleId}",
"team.update_role",
path={"roleId": role_id},
body=body,
timeout=timeout,
),
)

def delete_role(
self,
role_id: str,
query: DeleteTeamRoleQuery | None = None,
*,
timeout: float | None = None,
) -> MessageResponse:
"""Deletes a custom role. Built-in roles cannot be deleted.

While the role is still assigned to members, invites or SCIM groups, pass
``{"replacement_role_id": ...}``: the role that receives those assignments.
"""
return cast(
MessageResponse,
self._request(
"DELETE /team/roles/{roleId}",
"team.delete_role",
path={"roleId": role_id},
query=query,
timeout=timeout,
),
)

def duplicate_role(
self, role_id: str, body: DuplicateTeamRoleData, *, timeout: float | None = None
) -> TeamRoleData:
"""Copies a role under a new name. The Owner role cannot be duplicated.

Without a ``description``, the copy keeps the description of the source role.
"""
return cast(
TeamRoleData,
self._request(
"POST /team/roles/{roleId}/duplicate",
"team.duplicate_role",
path={"roleId": role_id},
body=body,
timeout=timeout,
),
)


class WebhookDeliveries(Resource):
"""Delivery attempts of a webhook. Team token."""
Expand Down
36 changes: 29 additions & 7 deletions tests/test_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,23 @@
("team.update", ({"name": "Acme"},), "PUT /team"),
("team.usage", (), "GET /team/usage"),
("team.roles", (), "GET /team/roles"),
("team.retrieve_role", ("r1",), "GET /team/roles/{roleId}"),
(
"team.create_role",
({"name": "Support", "permissions": ["support:manage"]},),
"POST /team/roles",
),
(
"team.update_role",
("r1", {"name": "Support", "permissions": ["support:manage"]}),
"PUT /team/roles/{roleId}",
),
("team.delete_role", ("r1",), "DELETE /team/roles/{roleId}"),
(
"team.duplicate_role",
("r1", {"name": "Support copy"}),
"POST /team/roles/{roleId}/duplicate",
),
("team.members.list", (), "GET /team/members"),
("team.members.iterate", (), "GET /team/members"),
("team.members.retrieve", ("u1",), "GET /team/members/{userId}"),
Expand Down Expand Up @@ -160,13 +177,8 @@
),
]

# Operations without a method yet: added by the next PRs in this stack.
# Operations without a method yet: added by the next PR in this stack.
PENDING = {
"GET /team/roles/{roleId}",
"POST /team/roles",
"PUT /team/roles/{roleId}",
"DELETE /team/roles/{roleId}",
"POST /team/roles/{roleId}/duplicate",
"DELETE /team/members/{userId}",
}

Expand Down Expand Up @@ -208,7 +220,17 @@
],
"stats": ["retrieve"],
"suppressions": ["create", "delete", "iterate", "list"],
"team": ["retrieve", "roles", "update", "usage"],
"team": [
"create_role",
"delete_role",
"duplicate_role",
"retrieve",
"retrieve_role",
"roles",
"update",
"update_role",
"usage",
],
"team.invites": ["create", "iterate", "list", "retrieve", "revoke", "update"],
"team.members": ["iterate", "list", "retrieve", "update_assignment"],
"webhooks": [
Expand Down
Loading
Loading