linq-python 0.12.0__tar.gz → 0.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linq_python-0.13.0/.release-please-manifest.json +3 -0
- linq_python-0.13.0/CHANGELOG.md +8 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/PKG-INFO +5 -3
- {linq_python-0.12.0 → linq_python-0.13.0}/README.md +2 -2
- {linq_python-0.12.0 → linq_python-0.13.0}/api.md +1 -1
- {linq_python-0.12.0 → linq_python-0.13.0}/pyproject.toml +2 -1
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/__init__.py +2 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_client.py +22 -2
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_exceptions.py +4 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_version.py +1 -1
- linq_python-0.13.0/src/linq/resources/webhooks.py +69 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/__init__.py +1 -1
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/sent_message.py +5 -2
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message.py +12 -3
- linq_python-0.12.0/src/linq/types/events_webhook_event.py → linq_python-0.13.0/src/linq/types/unwrap_webhook_event.py +2 -2
- linq_python-0.13.0/tests/api_resources/test_webhooks.py +103 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/uv.lock +156 -3
- linq_python-0.12.0/.release-please-manifest.json +0 -3
- linq_python-0.12.0/CHANGELOG.md +0 -8
- linq_python-0.12.0/src/linq/resources/webhooks.py +0 -34
- linq_python-0.12.0/tests/api_resources/test_webhooks.py +0 -19
- {linq_python-0.12.0 → linq_python-0.13.0}/.gitignore +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/CONTRIBUTING.md +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/LICENSE +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/SECURITY.md +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/bin/check-release-environment +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/bin/publish-pypi +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/examples/.keep +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/release-please-config.json +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/requirements-dev.lock +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_base_client.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_compat.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_constants.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_files.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_models.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_qs.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_resource.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_streaming.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_types.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_compat.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_datetime_parse.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_json.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_logs.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_path.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_proxy.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_reflection.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_resources_proxy.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_streams.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_sync.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_transform.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_typing.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/_utils/_utils.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/lib/.keep +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/pagination.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/py.typed +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/attachments.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/capability.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/chats/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/chats/chats.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/chats/location.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/chats/messages.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/chats/participants.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/chats/typing.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/contact_card.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/messages.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/phone_numbers.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/phonenumbers.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/webhook_events.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/resources/webhook_subscriptions.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/attachment_create_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/attachment_create_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/attachment_retrieve_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/capability_check_RCS_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/capability_check_i_message_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_create_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_create_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_created_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_group_icon_update_failed_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_group_icon_updated_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_group_name_update_failed_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_group_name_updated_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_leave_chat_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_list_chats_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_send_voicememo_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_send_voicememo_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_typing_indicator_started_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_typing_indicator_stopped_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_update_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chat_update_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/get_chat_location_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/location_request_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/message_list_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/message_send_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/message_send_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/participant_add_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/participant_add_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/participant_remove_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/chats/participant_remove_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/contact_card_create_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/contact_card_retrieve_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/contact_card_retrieve_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/contact_card_update_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/handle_check_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/link_part_param.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/media_part_param.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_add_reaction_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_add_reaction_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_content_param.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_delivered_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_edited_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_effect.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_effect_param.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_event_v2.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_failed_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_list_messages_thread_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_read_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_received_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_sent_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/message_update_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/participant_added_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/participant_removed_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/phone_number_list_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/phone_number_status_updated_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/phonenumber_list_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/reaction_added_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/reaction_event_base.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/reaction_removed_webhook_event.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/reply_to.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/reply_to_param.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/schemas_media_part_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/schemas_message_effect.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/schemas_text_part_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/set_contact_card.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/chat_handle.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/link_part_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/media_part_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/reaction.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/reaction_type.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/service_type.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/text_decoration.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared/text_part_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared_params/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared_params/reaction_type.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared_params/service_type.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/shared_params/text_decoration.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/supported_content_type.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/text_part_param.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/webhook_event_list_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/webhook_event_type.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/webhook_subscription.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/webhook_subscription_create_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/webhook_subscription_create_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/webhook_subscription_list_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/src/linq/types/webhook_subscription_update_params.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/chats/__init__.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/chats/test_location.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/chats/test_messages.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/chats/test_participants.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/chats/test_typing.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_attachments.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_capability.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_chats.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_contact_card.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_messages.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_phone_numbers.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_phonenumbers.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_webhook_events.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/api_resources/test_webhook_subscriptions.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/conftest.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/sample_file.txt +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_client.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_extract_files.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_files.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_models.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_qs.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_required_args.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_response.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_streaming.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_transform.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_utils/test_datetime_parse.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_utils/test_json.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_utils/test_path.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_utils/test_proxy.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/test_utils/test_typing.py +0 -0
- {linq_python-0.12.0 → linq_python-0.13.0}/tests/utils.py +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.13.0](https://github.com/linq-team/linq-python/compare/v0.12.0...v0.13.0) (2026-06-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add delivery_status field and deprecate is_read/is_delivered ([#23](https://github.com/linq-team/linq-python/issues/23)) ([0f5f6fa](https://github.com/linq-team/linq-python/commit/0f5f6fab581ea5fab03f4fe29abc916cf7a3eef4))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: linq-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: The official Python library for the linq-api-v3 API
|
|
5
5
|
Project-URL: Homepage, https://github.com/linq-team/linq-python
|
|
6
6
|
Project-URL: Repository, https://github.com/linq-team/linq-python
|
|
@@ -31,6 +31,8 @@ Requires-Dist: typing-extensions<5,>=4.14
|
|
|
31
31
|
Provides-Extra: aiohttp
|
|
32
32
|
Requires-Dist: aiohttp; extra == 'aiohttp'
|
|
33
33
|
Requires-Dist: httpx-aiohttp>=0.1.9; extra == 'aiohttp'
|
|
34
|
+
Provides-Extra: webhooks
|
|
35
|
+
Requires-Dist: standardwebhooks<2,>=1.0.1; extra == 'webhooks'
|
|
34
36
|
Description-Content-Type: text/markdown
|
|
35
37
|
|
|
36
38
|
# Linq API V3 Python API library
|
|
@@ -48,8 +50,8 @@ It is generated with [Stainless](https://www.stainless.com/).
|
|
|
48
50
|
|
|
49
51
|
Use the Linq API V3 MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
|
|
50
52
|
|
|
51
|
-
[](https://cursor.com/en-US/install-mcp?name=%40linqapp%2Fsdk-mcp&config=
|
|
52
|
-
[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40linqapp%2Fsdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40linqapp%2Fsdk-mcp%22%5D%2C%22env%22%3A%7B%22LINQ_API_V3_API_KEY%22%3A%22My%20API%20Key%22%7D%7D)
|
|
53
|
+
[](https://cursor.com/en-US/install-mcp?name=%40linqapp%2Fsdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaW5xYXBwL3Nkay1tY3AiXSwiZW52Ijp7IkxJTlFfQVBJX1YzX0FQSV9LRVkiOiJNeSBBUEkgS2V5IiwiTElOUV9XRUJIT09LX1NFQ1JFVCI6Ik15IFdlYmhvb2sgU2VjcmV0In19)
|
|
54
|
+
[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40linqapp%2Fsdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40linqapp%2Fsdk-mcp%22%5D%2C%22env%22%3A%7B%22LINQ_API_V3_API_KEY%22%3A%22My%20API%20Key%22%2C%22LINQ_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Secret%22%7D%7D)
|
|
53
55
|
|
|
54
56
|
> Note: You may need to set environment variables in your MCP client.
|
|
55
57
|
|
|
@@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/).
|
|
|
13
13
|
|
|
14
14
|
Use the Linq API V3 MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
|
|
15
15
|
|
|
16
|
-
[](https://cursor.com/en-US/install-mcp?name=%40linqapp%2Fsdk-mcp&config=
|
|
17
|
-
[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40linqapp%2Fsdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40linqapp%2Fsdk-mcp%22%5D%2C%22env%22%3A%7B%22LINQ_API_V3_API_KEY%22%3A%22My%20API%20Key%22%7D%7D)
|
|
16
|
+
[](https://cursor.com/en-US/install-mcp?name=%40linqapp%2Fsdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaW5xYXBwL3Nkay1tY3AiXSwiZW52Ijp7IkxJTlFfQVBJX1YzX0FQSV9LRVkiOiJNeSBBUEkgS2V5IiwiTElOUV9XRUJIT09LX1NFQ1JFVCI6Ik15IFdlYmhvb2sgU2VjcmV0In19)
|
|
17
|
+
[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40linqapp%2Fsdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40linqapp%2Fsdk-mcp%22%5D%2C%22env%22%3A%7B%22LINQ_API_V3_API_KEY%22%3A%22My%20API%20Key%22%2C%22LINQ_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Secret%22%7D%7D)
|
|
18
18
|
|
|
19
19
|
> Note: You may need to set environment variables in your MCP client.
|
|
20
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "linq-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.0"
|
|
4
4
|
description = "The official Python library for the linq-api-v3 API"
|
|
5
5
|
dynamic = ["readme"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -42,6 +42,7 @@ Repository = "https://github.com/linq-team/linq-python"
|
|
|
42
42
|
|
|
43
43
|
[project.optional-dependencies]
|
|
44
44
|
aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]
|
|
45
|
+
webhooks = ["standardwebhooks >= 1.0.1, < 2"]
|
|
45
46
|
|
|
46
47
|
[tool.uv]
|
|
47
48
|
managed = true
|
|
@@ -34,6 +34,7 @@ from ._exceptions import (
|
|
|
34
34
|
InternalServerError,
|
|
35
35
|
PermissionDeniedError,
|
|
36
36
|
UnprocessableEntityError,
|
|
37
|
+
APIWebhookValidationError,
|
|
37
38
|
APIResponseValidationError,
|
|
38
39
|
)
|
|
39
40
|
from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
|
|
@@ -57,6 +58,7 @@ __all__ = [
|
|
|
57
58
|
"APITimeoutError",
|
|
58
59
|
"APIConnectionError",
|
|
59
60
|
"APIResponseValidationError",
|
|
61
|
+
"APIWebhookValidationError",
|
|
60
62
|
"BadRequestError",
|
|
61
63
|
"AuthenticationError",
|
|
62
64
|
"PermissionDeniedError",
|
|
@@ -73,11 +73,13 @@ __all__ = [
|
|
|
73
73
|
class LinqAPIV3(SyncAPIClient):
|
|
74
74
|
# client options
|
|
75
75
|
api_key: str
|
|
76
|
+
webhook_secret: str | None
|
|
76
77
|
|
|
77
78
|
def __init__(
|
|
78
79
|
self,
|
|
79
80
|
*,
|
|
80
81
|
api_key: str | None = None,
|
|
82
|
+
webhook_secret: str | None = None,
|
|
81
83
|
base_url: str | httpx.URL | None = None,
|
|
82
84
|
timeout: float | Timeout | None | NotGiven = not_given,
|
|
83
85
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
@@ -99,7 +101,9 @@ class LinqAPIV3(SyncAPIClient):
|
|
|
99
101
|
) -> None:
|
|
100
102
|
"""Construct a new synchronous LinqAPIV3 client instance.
|
|
101
103
|
|
|
102
|
-
This automatically infers the
|
|
104
|
+
This automatically infers the following arguments from their corresponding environment variables if they are not provided:
|
|
105
|
+
- `api_key` from `LINQ_API_V3_API_KEY`
|
|
106
|
+
- `webhook_secret` from `LINQ_WEBHOOK_SECRET`
|
|
103
107
|
"""
|
|
104
108
|
if api_key is None:
|
|
105
109
|
api_key = os.environ.get("LINQ_API_V3_API_KEY")
|
|
@@ -109,6 +113,10 @@ class LinqAPIV3(SyncAPIClient):
|
|
|
109
113
|
)
|
|
110
114
|
self.api_key = api_key
|
|
111
115
|
|
|
116
|
+
if webhook_secret is None:
|
|
117
|
+
webhook_secret = os.environ.get("LINQ_WEBHOOK_SECRET")
|
|
118
|
+
self.webhook_secret = webhook_secret
|
|
119
|
+
|
|
112
120
|
if base_url is None:
|
|
113
121
|
base_url = os.environ.get("LINQ_API_V3_BASE_URL")
|
|
114
122
|
if base_url is None:
|
|
@@ -712,6 +720,7 @@ class LinqAPIV3(SyncAPIClient):
|
|
|
712
720
|
self,
|
|
713
721
|
*,
|
|
714
722
|
api_key: str | None = None,
|
|
723
|
+
webhook_secret: str | None = None,
|
|
715
724
|
base_url: str | httpx.URL | None = None,
|
|
716
725
|
timeout: float | Timeout | None | NotGiven = not_given,
|
|
717
726
|
http_client: httpx.Client | None = None,
|
|
@@ -746,6 +755,7 @@ class LinqAPIV3(SyncAPIClient):
|
|
|
746
755
|
http_client = http_client or self._client
|
|
747
756
|
return self.__class__(
|
|
748
757
|
api_key=api_key or self.api_key,
|
|
758
|
+
webhook_secret=webhook_secret or self.webhook_secret,
|
|
749
759
|
base_url=base_url or self.base_url,
|
|
750
760
|
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
|
|
751
761
|
http_client=http_client,
|
|
@@ -796,11 +806,13 @@ class LinqAPIV3(SyncAPIClient):
|
|
|
796
806
|
class AsyncLinqAPIV3(AsyncAPIClient):
|
|
797
807
|
# client options
|
|
798
808
|
api_key: str
|
|
809
|
+
webhook_secret: str | None
|
|
799
810
|
|
|
800
811
|
def __init__(
|
|
801
812
|
self,
|
|
802
813
|
*,
|
|
803
814
|
api_key: str | None = None,
|
|
815
|
+
webhook_secret: str | None = None,
|
|
804
816
|
base_url: str | httpx.URL | None = None,
|
|
805
817
|
timeout: float | Timeout | None | NotGiven = not_given,
|
|
806
818
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
@@ -822,7 +834,9 @@ class AsyncLinqAPIV3(AsyncAPIClient):
|
|
|
822
834
|
) -> None:
|
|
823
835
|
"""Construct a new async AsyncLinqAPIV3 client instance.
|
|
824
836
|
|
|
825
|
-
This automatically infers the
|
|
837
|
+
This automatically infers the following arguments from their corresponding environment variables if they are not provided:
|
|
838
|
+
- `api_key` from `LINQ_API_V3_API_KEY`
|
|
839
|
+
- `webhook_secret` from `LINQ_WEBHOOK_SECRET`
|
|
826
840
|
"""
|
|
827
841
|
if api_key is None:
|
|
828
842
|
api_key = os.environ.get("LINQ_API_V3_API_KEY")
|
|
@@ -832,6 +846,10 @@ class AsyncLinqAPIV3(AsyncAPIClient):
|
|
|
832
846
|
)
|
|
833
847
|
self.api_key = api_key
|
|
834
848
|
|
|
849
|
+
if webhook_secret is None:
|
|
850
|
+
webhook_secret = os.environ.get("LINQ_WEBHOOK_SECRET")
|
|
851
|
+
self.webhook_secret = webhook_secret
|
|
852
|
+
|
|
835
853
|
if base_url is None:
|
|
836
854
|
base_url = os.environ.get("LINQ_API_V3_BASE_URL")
|
|
837
855
|
if base_url is None:
|
|
@@ -1435,6 +1453,7 @@ class AsyncLinqAPIV3(AsyncAPIClient):
|
|
|
1435
1453
|
self,
|
|
1436
1454
|
*,
|
|
1437
1455
|
api_key: str | None = None,
|
|
1456
|
+
webhook_secret: str | None = None,
|
|
1438
1457
|
base_url: str | httpx.URL | None = None,
|
|
1439
1458
|
timeout: float | Timeout | None | NotGiven = not_given,
|
|
1440
1459
|
http_client: httpx.AsyncClient | None = None,
|
|
@@ -1469,6 +1488,7 @@ class AsyncLinqAPIV3(AsyncAPIClient):
|
|
|
1469
1488
|
http_client = http_client or self._client
|
|
1470
1489
|
return self.__class__(
|
|
1471
1490
|
api_key=api_key or self.api_key,
|
|
1491
|
+
webhook_secret=webhook_secret or self.webhook_secret,
|
|
1472
1492
|
base_url=base_url or self.base_url,
|
|
1473
1493
|
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
|
|
1474
1494
|
http_client=http_client,
|
|
@@ -54,6 +54,10 @@ class APIResponseValidationError(APIError):
|
|
|
54
54
|
self.status_code = response.status_code
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
class APIWebhookValidationError(APIError):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
57
61
|
class APIStatusError(APIError):
|
|
58
62
|
"""Raised when an API response has a status code of 4xx or 5xx."""
|
|
59
63
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Mapping, cast
|
|
7
|
+
|
|
8
|
+
from .._models import construct_type
|
|
9
|
+
from .._resource import SyncAPIResource, AsyncAPIResource
|
|
10
|
+
from .._exceptions import LinqApiv3Error
|
|
11
|
+
from ..types.unwrap_webhook_event import UnwrapWebhookEvent
|
|
12
|
+
|
|
13
|
+
__all__ = ["WebhooksResource", "AsyncWebhooksResource"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WebhooksResource(SyncAPIResource):
|
|
17
|
+
def unwrap(self, payload: str, *, headers: Mapping[str, str], key: str | bytes | None = None) -> UnwrapWebhookEvent:
|
|
18
|
+
try:
|
|
19
|
+
from standardwebhooks import Webhook
|
|
20
|
+
except ImportError as exc:
|
|
21
|
+
raise LinqApiv3Error("You need to install `linq-python[webhooks]` to use this method") from exc
|
|
22
|
+
|
|
23
|
+
if key is None:
|
|
24
|
+
key = self._client.webhook_secret
|
|
25
|
+
if key is None:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"Cannot verify a webhook without a key on either the client's webhook_secret or passed in as an argument"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if not isinstance(headers, dict):
|
|
31
|
+
headers = dict(headers)
|
|
32
|
+
|
|
33
|
+
Webhook(key).verify(payload, headers)
|
|
34
|
+
|
|
35
|
+
return cast(
|
|
36
|
+
UnwrapWebhookEvent,
|
|
37
|
+
construct_type(
|
|
38
|
+
type_=UnwrapWebhookEvent,
|
|
39
|
+
value=json.loads(payload),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AsyncWebhooksResource(AsyncAPIResource):
|
|
45
|
+
def unwrap(self, payload: str, *, headers: Mapping[str, str], key: str | bytes | None = None) -> UnwrapWebhookEvent:
|
|
46
|
+
try:
|
|
47
|
+
from standardwebhooks import Webhook
|
|
48
|
+
except ImportError as exc:
|
|
49
|
+
raise LinqApiv3Error("You need to install `linq-python[webhooks]` to use this method") from exc
|
|
50
|
+
|
|
51
|
+
if key is None:
|
|
52
|
+
key = self._client.webhook_secret
|
|
53
|
+
if key is None:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"Cannot verify a webhook without a key on either the client's webhook_secret or passed in as an argument"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not isinstance(headers, dict):
|
|
59
|
+
headers = dict(headers)
|
|
60
|
+
|
|
61
|
+
Webhook(key).verify(payload, headers)
|
|
62
|
+
|
|
63
|
+
return cast(
|
|
64
|
+
UnwrapWebhookEvent,
|
|
65
|
+
construct_type(
|
|
66
|
+
type_=UnwrapWebhookEvent,
|
|
67
|
+
value=json.loads(payload),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
@@ -28,8 +28,8 @@ from .webhook_event_type import WebhookEventType as WebhookEventType
|
|
|
28
28
|
from .reaction_event_base import ReactionEventBase as ReactionEventBase
|
|
29
29
|
from .chat_create_response import ChatCreateResponse as ChatCreateResponse
|
|
30
30
|
from .chat_update_response import ChatUpdateResponse as ChatUpdateResponse
|
|
31
|
-
from .events_webhook_event import EventsWebhookEvent as EventsWebhookEvent
|
|
32
31
|
from .message_effect_param import MessageEffectParam as MessageEffectParam
|
|
32
|
+
from .unwrap_webhook_event import UnwrapWebhookEvent as UnwrapWebhookEvent
|
|
33
33
|
from .webhook_subscription import WebhookSubscription as WebhookSubscription
|
|
34
34
|
from .handle_check_response import HandleCheckResponse as HandleCheckResponse
|
|
35
35
|
from .message_content_param import MessageContentParam as MessageContentParam
|
|
@@ -27,11 +27,14 @@ class SentMessage(BaseModel):
|
|
|
27
27
|
created_at: datetime
|
|
28
28
|
"""When the message was created"""
|
|
29
29
|
|
|
30
|
-
delivery_status: Literal["pending", "queued", "sent", "delivered", "failed"]
|
|
30
|
+
delivery_status: Literal["pending", "queued", "sent", "delivered", "received", "read", "failed"]
|
|
31
31
|
"""Current delivery status of a message"""
|
|
32
32
|
|
|
33
33
|
is_read: bool
|
|
34
|
-
"""
|
|
34
|
+
"""DEPRECATED: Use `delivery_status == "read"` instead.
|
|
35
|
+
|
|
36
|
+
Whether the message has been read.
|
|
37
|
+
"""
|
|
35
38
|
|
|
36
39
|
parts: List[Part]
|
|
37
40
|
"""Message parts in order (text, media, and link)"""
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import List, Union, Optional
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from typing_extensions import TypeAlias
|
|
5
|
+
from typing_extensions import Literal, TypeAlias
|
|
6
6
|
|
|
7
7
|
from pydantic import Field as FieldInfo
|
|
8
8
|
|
|
@@ -30,14 +30,23 @@ class Message(BaseModel):
|
|
|
30
30
|
created_at: datetime
|
|
31
31
|
"""When the message was created"""
|
|
32
32
|
|
|
33
|
+
delivery_status: Literal["pending", "queued", "sent", "delivered", "received", "read", "failed"]
|
|
34
|
+
"""Current delivery status of a message"""
|
|
35
|
+
|
|
33
36
|
is_delivered: bool
|
|
34
|
-
"""
|
|
37
|
+
"""
|
|
38
|
+
DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is
|
|
39
|
+
`delivered` or `read`). Whether the message has been delivered.
|
|
40
|
+
"""
|
|
35
41
|
|
|
36
42
|
is_from_me: bool
|
|
37
43
|
"""Whether this message was sent by the authenticated user"""
|
|
38
44
|
|
|
39
45
|
is_read: bool
|
|
40
|
-
"""
|
|
46
|
+
"""DEPRECATED: Use `delivery_status == "read"` instead.
|
|
47
|
+
|
|
48
|
+
Whether the message has been read.
|
|
49
|
+
"""
|
|
41
50
|
|
|
42
51
|
updated_at: datetime
|
|
43
52
|
"""When the message was last updated"""
|
|
@@ -23,9 +23,9 @@ from .chat_group_name_update_failed_webhook_event import ChatGroupNameUpdateFail
|
|
|
23
23
|
from .chat_typing_indicator_started_webhook_event import ChatTypingIndicatorStartedWebhookEvent
|
|
24
24
|
from .chat_typing_indicator_stopped_webhook_event import ChatTypingIndicatorStoppedWebhookEvent
|
|
25
25
|
|
|
26
|
-
__all__ = ["
|
|
26
|
+
__all__ = ["UnwrapWebhookEvent"]
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
UnwrapWebhookEvent: TypeAlias = Annotated[
|
|
29
29
|
Union[
|
|
30
30
|
MessageSentWebhookEvent,
|
|
31
31
|
MessageReceivedWebhookEvent,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import standardwebhooks
|
|
10
|
+
|
|
11
|
+
from linq import LinqAPIV3
|
|
12
|
+
|
|
13
|
+
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestWebhooks:
|
|
17
|
+
parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize(
|
|
20
|
+
"client_opt,method_opt",
|
|
21
|
+
[
|
|
22
|
+
("whsec_c2VjcmV0Cg==", None),
|
|
23
|
+
("wrong", b"secret\n"),
|
|
24
|
+
("wrong", "whsec_c2VjcmV0Cg=="),
|
|
25
|
+
(None, b"secret\n"),
|
|
26
|
+
(None, "whsec_c2VjcmV0Cg=="),
|
|
27
|
+
],
|
|
28
|
+
)
|
|
29
|
+
def test_method_unwrap(self, client: LinqAPIV3, client_opt: str | None, method_opt: str | bytes | None) -> None:
|
|
30
|
+
hook = standardwebhooks.Webhook(b"secret\n")
|
|
31
|
+
|
|
32
|
+
client = client.with_options(webhook_secret=client_opt)
|
|
33
|
+
|
|
34
|
+
data = """{"api_version":"v3","created_at":"2025-11-23T17:30:00Z","data":{"id":"550e8400-e29b-41d4-a716-446655440001","chat":{"id":"550e8400-e29b-41d4-a716-446655440000","health_status":{"doc_url":"https://docs.linqapp.com/guides/chats/chat-health#at-risk","status":"AT_RISK","updated_at":"2026-05-01T18:28:25Z"},"is_group":true,"owner_handle":{"id":"550e8400-e29b-41d4-a716-446655440000","handle":"+15551234567","joined_at":"2025-05-21T15:30:00.000-05:00","service":"iMessage","is_me":false,"left_at":"2019-12-27T18:11:19.117Z","status":"active"}},"direction":"outbound","parts":[{"type":"text","value":"Hello!","text_decorations":[{"range":[0,5],"animation":"shake","style":"bold"}]}],"sender_handle":{"id":"550e8400-e29b-41d4-a716-446655440000","handle":"+15551234567","joined_at":"2025-05-21T15:30:00.000-05:00","service":"iMessage","is_me":false,"left_at":"2019-12-27T18:11:19.117Z","status":"active"},"service":"iMessage","delivered_at":"2026-01-30T20:49:20.352Z","effect":{"name":"gentle","type":"bubble"},"idempotency_key":"unique-key","preferred_service":"iMessage","read_at":null,"reply_to":{"message_id":"182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e","part_index":0},"sent_at":"2026-01-30T20:49:19.704Z"},"event_id":"550e8400-e29b-41d4-a716-446655440000","event_type":"message.sent","partner_id":"partner_abc123","trace_id":"abc123def456","webhook_version":"2025-01-01"}"""
|
|
35
|
+
msg_id = "1"
|
|
36
|
+
timestamp = datetime.now(tz=timezone.utc)
|
|
37
|
+
sig = hook.sign(msg_id=msg_id, timestamp=timestamp, data=data)
|
|
38
|
+
headers = {
|
|
39
|
+
"webhook-id": msg_id,
|
|
40
|
+
"webhook-timestamp": str(int(timestamp.timestamp())),
|
|
41
|
+
"webhook-signature": sig,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
_ = client.webhooks.unwrap(data, headers=headers, key=method_opt)
|
|
46
|
+
except standardwebhooks.WebhookVerificationError as e:
|
|
47
|
+
raise AssertionError("Failed to unwrap valid webhook") from e
|
|
48
|
+
|
|
49
|
+
bad_headers = [
|
|
50
|
+
{**headers, "webhook-signature": hook.sign(msg_id=msg_id, timestamp=timestamp, data="xxx")},
|
|
51
|
+
{**headers, "webhook-id": "bad"},
|
|
52
|
+
{**headers, "webhook-timestamp": "0"},
|
|
53
|
+
]
|
|
54
|
+
for bad_header in bad_headers:
|
|
55
|
+
with pytest.raises(standardwebhooks.WebhookVerificationError):
|
|
56
|
+
_ = client.webhooks.unwrap(data, headers=bad_header, key=method_opt)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestAsyncWebhooks:
|
|
60
|
+
parametrize = pytest.mark.parametrize(
|
|
61
|
+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize(
|
|
65
|
+
"client_opt,method_opt",
|
|
66
|
+
[
|
|
67
|
+
("whsec_c2VjcmV0Cg==", None),
|
|
68
|
+
("wrong", b"secret\n"),
|
|
69
|
+
("wrong", "whsec_c2VjcmV0Cg=="),
|
|
70
|
+
(None, b"secret\n"),
|
|
71
|
+
(None, "whsec_c2VjcmV0Cg=="),
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
def test_method_unwrap(
|
|
75
|
+
self, async_client: LinqAPIV3, client_opt: str | None, method_opt: str | bytes | None
|
|
76
|
+
) -> None:
|
|
77
|
+
hook = standardwebhooks.Webhook(b"secret\n")
|
|
78
|
+
|
|
79
|
+
async_client = async_client.with_options(webhook_secret=client_opt)
|
|
80
|
+
|
|
81
|
+
data = """{"api_version":"v3","created_at":"2025-11-23T17:30:00Z","data":{"id":"550e8400-e29b-41d4-a716-446655440001","chat":{"id":"550e8400-e29b-41d4-a716-446655440000","health_status":{"doc_url":"https://docs.linqapp.com/guides/chats/chat-health#at-risk","status":"AT_RISK","updated_at":"2026-05-01T18:28:25Z"},"is_group":true,"owner_handle":{"id":"550e8400-e29b-41d4-a716-446655440000","handle":"+15551234567","joined_at":"2025-05-21T15:30:00.000-05:00","service":"iMessage","is_me":false,"left_at":"2019-12-27T18:11:19.117Z","status":"active"}},"direction":"outbound","parts":[{"type":"text","value":"Hello!","text_decorations":[{"range":[0,5],"animation":"shake","style":"bold"}]}],"sender_handle":{"id":"550e8400-e29b-41d4-a716-446655440000","handle":"+15551234567","joined_at":"2025-05-21T15:30:00.000-05:00","service":"iMessage","is_me":false,"left_at":"2019-12-27T18:11:19.117Z","status":"active"},"service":"iMessage","delivered_at":"2026-01-30T20:49:20.352Z","effect":{"name":"gentle","type":"bubble"},"idempotency_key":"unique-key","preferred_service":"iMessage","read_at":null,"reply_to":{"message_id":"182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e","part_index":0},"sent_at":"2026-01-30T20:49:19.704Z"},"event_id":"550e8400-e29b-41d4-a716-446655440000","event_type":"message.sent","partner_id":"partner_abc123","trace_id":"abc123def456","webhook_version":"2025-01-01"}"""
|
|
82
|
+
msg_id = "1"
|
|
83
|
+
timestamp = datetime.now(tz=timezone.utc)
|
|
84
|
+
sig = hook.sign(msg_id=msg_id, timestamp=timestamp, data=data)
|
|
85
|
+
headers = {
|
|
86
|
+
"webhook-id": msg_id,
|
|
87
|
+
"webhook-timestamp": str(int(timestamp.timestamp())),
|
|
88
|
+
"webhook-signature": sig,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
_ = async_client.webhooks.unwrap(data, headers=headers, key=method_opt)
|
|
93
|
+
except standardwebhooks.WebhookVerificationError as e:
|
|
94
|
+
raise AssertionError("Failed to unwrap valid webhook") from e
|
|
95
|
+
|
|
96
|
+
bad_headers = [
|
|
97
|
+
{**headers, "webhook-signature": hook.sign(msg_id=msg_id, timestamp=timestamp, data="xxx")},
|
|
98
|
+
{**headers, "webhook-id": "bad"},
|
|
99
|
+
{**headers, "webhook-timestamp": "0"},
|
|
100
|
+
]
|
|
101
|
+
for bad_header in bad_headers:
|
|
102
|
+
with pytest.raises(standardwebhooks.WebhookVerificationError):
|
|
103
|
+
_ = async_client.webhooks.unwrap(data, headers=bad_header, key=method_opt)
|