python(feat): webhooks resource - #789
Brandon-Shippy wants to merge 8 commits into
Conversation
|
Python docs preview: https://sift-stack.github.io/sift/python/pr-789/ Deployed from |
| return cls(int(val)) | ||
|
|
||
|
|
||
| class RuleAction(BaseType[RuleActionProto, "RuleAction"]): |
There was a problem hiding this comment.
This is a pretty inconvenient type to work with and doesn't follow the user-friendly patterns we do with other types. That is using the to_proto to convert to the proto friendly Ids. Can we address this as par tof this PR instead of carrying the tech debt forward?
| @@ -0,0 +1,62 @@ | |||
| """References to other Sift resources. | |||
There was a problem hiding this comment.
Ref[T] is just str | T with a new name to look up. Let's write the union inline like the other models do and drop this
There was a problem hiding this comment.
i removed it union is now inline
| return cls(action_type=RuleActionType.WEBHOOK, webhook=webhook) | ||
|
|
||
| @classmethod | ||
| def for_annotation( |
There was a problem hiding this comment.
Renaming RuleAction.annotation breaks existing callers. Let's keep it as is and name the new constructor RuleAction.webhook
in general, renaming existing fields is not backwards compatible and can cause unintentional/breaking changes (e.g., rule.action.tags now returns something else
There was a problem hiding this comment.
I kept RuleAction.annotation, tags_ids, default_assignee_user, and the tags property
|
|
||
| #### Webhooks API | ||
|
|
||
| New `client.webhooks` resource, and `RuleAction.for_webhook(...)` to attach a webhook to a rule. |
There was a problem hiding this comment.
make sure the changelog is updated to reflect any changes
There was a problem hiding this comment.
added logs for user-visible changes
There was a problem hiding this comment.
pre-existing, but lets allow asset objects here too (list[str | Asset]) so callers don't need asset._id_or_error
There was a problem hiding this comment.
asset_ids now accepts list[str | Asset]
2baa0a6 to
cecc9b0
Compare
cecc9b0 to
a6ddb67
Compare
alexluck-sift
left a comment
There was a problem hiding this comment.
Audit pass on the webhooks surface. Nine comments, five of them nits. The first one is the only thing I'd call blocking, and it's a reviewer conflict rather than anything you did wrong.
| annotation_type: RuleAnnotationType | None = None | ||
| tags_ids: list[str] | None = None | ||
| default_assignee_user: str | None = None | ||
| webhook_id: str | None = None |
There was a problem hiding this comment.
RuleAction.webhook(wh) accepts an object, but the field it writes only takes a string, so anyone building a RuleAction directly is back to webhook._id_or_error. That's the thing my rule.py:277 thread asked to remove, and asset_ids gained list[str | Asset] 113 lines up in this same commit.
I'd take str | Webhook here and resolve it in _to_update_request. Worth settling with @wei-qlu first, since dropping the Ref alias was right but it took this with it. Same applies to tags_ids and default_assignee_user above, which are pre-existing so out of scope for this PR.
There was a problem hiding this comment.
RuleAction.webhook_id now just takes str | Webhook
| http_headers: list[WebhookHttpHeader] | ||
| created_date: datetime | ||
| modified_date: datetime | ||
| created_by_user_id: str |
There was a problem hiding this comment.
created_by_user_id, modified_by_user_id and organization_id have no accessor, so finding out who made a webhook is a second call the caller assembles. Rule.assets and Channel.asset are the shape: an ID field plus a property that resolves it.
Add created_by and modified_by backed by client.users. Heads up that the seven existing created_by/organization properties in sift_types/ all raise NotImplementedError, written before the Users resource existed, so they look like precedent and aren't.
There was a problem hiding this comment.
added Webhook.created_by and modified_by 38c50f7
| webhook = await self._low_level_client.get_webhook(webhook_id=webhook_id) | ||
| return self._apply_client_to_instance(webhook) | ||
|
|
||
| async def list_( |
There was a problem hiding this comment.
No date ranges and no created_by/modified_by, so "webhooks added last week" isn't expressible. ListWebhooksRequest only filters on webhook_id, name, event_type, is_archived and archived_date, and CONTRIBUTING says a proto that can't serve the standard filter set should be changed rather than worked around.
Add created_date, modified_date, created_by_user_id and modified_by_user_id to the ListWebhooks filter fields, then expose the standard arguments here.
There was a problem hiding this comment.
ListWebhooks only supports webhook_id, name, event_type, is_archived, archived_date. Cannot filter this currently. I will make a ticket to fix this
| grpc_webhook = cast("CreateWebhookResponse", response).webhook | ||
| return Webhook._from_proto(grpc_webhook) | ||
|
|
||
| async def update_webhook(self, update: WebhookUpdate) -> Webhook: |
There was a problem hiding this comment.
Two concurrent webhooks.update calls each send a full Webhook here, so the second one reverts the first one's field. This is the only MergeMessage in low_level_wrappers/, and the proto declares a sparse masked patch with seven maskable fields named.
I'd send the patch and mask straight through. If the service genuinely rejects a sparse Webhook, file that as a server bug and link it from here, because the workaround costs a round trip and loses concurrent writes.
There was a problem hiding this comment.
Server-side bug. Making a ticket and will triage. UpdateWebhook cannot accept a partial webhook. So, it reads the webhook before writing the update. Concurrent webhooks will overwrite each other.
| """ | ||
| return await self.update(webhook, WebhookUpdate(is_archived=False)) | ||
|
|
||
| async def test( |
There was a problem hiding this comment.
nit: this sends a real HTTP request to a customer endpoint, and test reads like the opposite to anyone scanning autocomplete, on top of colliding with test-suite vocabulary. webhook is also optional and positional, which fixes the parameter position permanently.
I'd go with send_test_request and move webhook behind the *. The rename touches sift_types/webhook.py and the sync stubs, so it needs doing by hand.
There was a problem hiding this comment.
renamed test to send_test_request 38c50f7
| class WebhookEventType(Enum): | ||
| """Enum for the events that trigger a webhook.""" | ||
|
|
||
| UNSPECIFIED = WebhookEventTypeProto.WEBHOOK_EVENT_TYPE_UNSPECIFIED # 0 |
There was a problem hiding this comment.
nit: WEBHOOK_EVENT_TYPE_UNSPECIFIED is the proto's zero value and never something a caller should pass. No other enum in sift_types/ exposes its unspecified member.
Drop it and map an unset event_type to None in _from_proto.
| """ | ||
|
|
||
| http_response_code: int | ||
| http_response_body: bytes |
There was a problem hiding this comment.
nit: bytes is the right call for the field, since the body comes from an arbitrary endpoint and may not be text at all, but it leaves every caller writing the same decode.
Add a text property returning self.http_response_body.decode(errors="replace"), matching AnnotationCommentElement.text in #792.
| return cls(name=proto.name, value=proto.value) | ||
|
|
||
|
|
||
| class WebhookTestResult(BaseModel): |
There was a problem hiding this comment.
There was a problem hiding this comment.
Decided on dropping the suffix
| return value | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_target_url(self): |
There was a problem hiding this comment.
nit: this validator sits on WebhookBase, which doesn't declare target_url, so it reads through getattr(self, "target_url", None). Rename the field on either subclass and validation silently passes everything, with a bad URL only failing at the server.
Declare target_url: str | None = None on the base so the attribute access is checked.
There was a problem hiding this comment.
the URL check is field_validator on each subclass
Description
Adds a webhooks resource to
sift_client, and lets a rule call a webhook when it is violated.Webhook and all associated classes and enums are made.
Implements get, list_, find, create, update, archive/unarchive, send_test_request
Validation