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
8 changes: 0 additions & 8 deletions CHANGELOG.md

This file was deleted.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/).

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.

[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40linqapp%2Fsdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaW5xYXBwL3Nkay1tY3AiXSwiZW52Ijp7IkxJTlFfQVBJX1YzX0FQSV9LRVkiOiJNeSBBUEkgS2V5In19)
[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](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)
[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40linqapp%2Fsdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaW5xYXBwL3Nkay1tY3AiXSwiZW52Ijp7IkxJTlFfQVBJX1YzX0FQSV9LRVkiOiJNeSBBUEkgS2V5IiwiTElOUV9XRUJIT09LX1NFQ1JFVCI6Ik15IFdlYmhvb2sgU2VjcmV0In19)
[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](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)

> Note: You may need to set environment variables in your MCP client.

Expand Down
2 changes: 1 addition & 1 deletion api.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ from linq.types import (
ChatTypingIndicatorStartedWebhookEvent,
ChatTypingIndicatorStoppedWebhookEvent,
PhoneNumberStatusUpdatedWebhookEvent,
EventsWebhookEvent,
UnwrapWebhookEvent,
)
```

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Repository = "https://github.com/linq-team/linq-python"

[project.optional-dependencies]
aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]
webhooks = ["standardwebhooks >= 1.0.1, < 2"]

[tool.uv]
managed = true
Expand Down
2 changes: 2 additions & 0 deletions src/linq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
InternalServerError,
PermissionDeniedError,
UnprocessableEntityError,
APIWebhookValidationError,
APIResponseValidationError,
)
from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
Expand All @@ -57,6 +58,7 @@
"APITimeoutError",
"APIConnectionError",
"APIResponseValidationError",
"APIWebhookValidationError",
"BadRequestError",
"AuthenticationError",
"PermissionDeniedError",
Expand Down
24 changes: 22 additions & 2 deletions src/linq/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@
class LinqAPIV3(SyncAPIClient):
# client options
api_key: str
webhook_secret: str | None

def __init__(
self,
*,
api_key: str | None = None,
webhook_secret: str | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
Expand All @@ -99,7 +101,9 @@ def __init__(
) -> None:
"""Construct a new synchronous LinqAPIV3 client instance.

This automatically infers the `api_key` argument from the `LINQ_API_V3_API_KEY` environment variable if it is not provided.
This automatically infers the following arguments from their corresponding environment variables if they are not provided:
- `api_key` from `LINQ_API_V3_API_KEY`
- `webhook_secret` from `LINQ_WEBHOOK_SECRET`
"""
if api_key is None:
api_key = os.environ.get("LINQ_API_V3_API_KEY")
Expand All @@ -109,6 +113,10 @@ def __init__(
)
self.api_key = api_key

if webhook_secret is None:
webhook_secret = os.environ.get("LINQ_WEBHOOK_SECRET")
self.webhook_secret = webhook_secret

if base_url is None:
base_url = os.environ.get("LINQ_API_V3_BASE_URL")
if base_url is None:
Expand Down Expand Up @@ -712,6 +720,7 @@ def copy(
self,
*,
api_key: str | None = None,
webhook_secret: str | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
Expand Down Expand Up @@ -746,6 +755,7 @@ def copy(
http_client = http_client or self._client
return self.__class__(
api_key=api_key or self.api_key,
webhook_secret=webhook_secret or self.webhook_secret,
base_url=base_url or self.base_url,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
http_client=http_client,
Expand Down Expand Up @@ -796,11 +806,13 @@ def _make_status_error(
class AsyncLinqAPIV3(AsyncAPIClient):
# client options
api_key: str
webhook_secret: str | None

def __init__(
self,
*,
api_key: str | None = None,
webhook_secret: str | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
Expand All @@ -822,7 +834,9 @@ def __init__(
) -> None:
"""Construct a new async AsyncLinqAPIV3 client instance.

This automatically infers the `api_key` argument from the `LINQ_API_V3_API_KEY` environment variable if it is not provided.
This automatically infers the following arguments from their corresponding environment variables if they are not provided:
- `api_key` from `LINQ_API_V3_API_KEY`
- `webhook_secret` from `LINQ_WEBHOOK_SECRET`
"""
if api_key is None:
api_key = os.environ.get("LINQ_API_V3_API_KEY")
Expand All @@ -832,6 +846,10 @@ def __init__(
)
self.api_key = api_key

if webhook_secret is None:
webhook_secret = os.environ.get("LINQ_WEBHOOK_SECRET")
self.webhook_secret = webhook_secret

if base_url is None:
base_url = os.environ.get("LINQ_API_V3_BASE_URL")
if base_url is None:
Expand Down Expand Up @@ -1435,6 +1453,7 @@ def copy(
self,
*,
api_key: str | None = None,
webhook_secret: str | None = None,
base_url: str | httpx.URL | None = None,
timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.AsyncClient | None = None,
Expand Down Expand Up @@ -1469,6 +1488,7 @@ def copy(
http_client = http_client or self._client
return self.__class__(
api_key=api_key or self.api_key,
webhook_secret=webhook_secret or self.webhook_secret,
base_url=base_url or self.base_url,
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
http_client=http_client,
Expand Down
4 changes: 4 additions & 0 deletions src/linq/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ def __init__(self, response: httpx.Response, body: object | None, *, message: st
self.status_code = response.status_code


class APIWebhookValidationError(APIError):
pass


class APIStatusError(APIError):
"""Raised when an API response has a status code of 4xx or 5xx."""

Expand Down
51 changes: 43 additions & 8 deletions src/linq/resources/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,67 @@
from __future__ import annotations

import json
from typing import cast
from typing import Mapping, cast

from .._models import construct_type
from .._resource import SyncAPIResource, AsyncAPIResource
from ..types.events_webhook_event import EventsWebhookEvent
from .._exceptions import LinqApiv3Error
from ..types.unwrap_webhook_event import UnwrapWebhookEvent

__all__ = ["WebhooksResource", "AsyncWebhooksResource"]


class WebhooksResource(SyncAPIResource):
def events(self, payload: str) -> EventsWebhookEvent:
def unwrap(self, payload: str, *, headers: Mapping[str, str], key: str | bytes | None = None) -> UnwrapWebhookEvent:
try:
from standardwebhooks import Webhook
except ImportError as exc:
raise LinqApiv3Error("You need to install `linq-python[webhooks]` to use this method") from exc

if key is None:
key = self._client.webhook_secret
if key is None:
raise ValueError(
"Cannot verify a webhook without a key on either the client's webhook_secret or passed in as an argument"
)

if not isinstance(headers, dict):
headers = dict(headers)

Webhook(key).verify(payload, headers)

return cast(
EventsWebhookEvent,
UnwrapWebhookEvent,
construct_type(
type_=EventsWebhookEvent,
type_=UnwrapWebhookEvent,
value=json.loads(payload),
),
)


class AsyncWebhooksResource(AsyncAPIResource):
def events(self, payload: str) -> EventsWebhookEvent:
def unwrap(self, payload: str, *, headers: Mapping[str, str], key: str | bytes | None = None) -> UnwrapWebhookEvent:
try:
from standardwebhooks import Webhook
except ImportError as exc:
raise LinqApiv3Error("You need to install `linq-python[webhooks]` to use this method") from exc

if key is None:
key = self._client.webhook_secret
if key is None:
raise ValueError(
"Cannot verify a webhook without a key on either the client's webhook_secret or passed in as an argument"
)

if not isinstance(headers, dict):
headers = dict(headers)

Webhook(key).verify(payload, headers)

return cast(
EventsWebhookEvent,
UnwrapWebhookEvent,
construct_type(
type_=EventsWebhookEvent,
type_=UnwrapWebhookEvent,
value=json.loads(payload),
),
)
2 changes: 1 addition & 1 deletion src/linq/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
from .reaction_event_base import ReactionEventBase as ReactionEventBase
from .chat_create_response import ChatCreateResponse as ChatCreateResponse
from .chat_update_response import ChatUpdateResponse as ChatUpdateResponse
from .events_webhook_event import EventsWebhookEvent as EventsWebhookEvent
from .message_effect_param import MessageEffectParam as MessageEffectParam
from .unwrap_webhook_event import UnwrapWebhookEvent as UnwrapWebhookEvent
from .webhook_subscription import WebhookSubscription as WebhookSubscription
from .handle_check_response import HandleCheckResponse as HandleCheckResponse
from .message_content_param import MessageContentParam as MessageContentParam
Expand Down
7 changes: 5 additions & 2 deletions src/linq/types/chats/sent_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ class SentMessage(BaseModel):
created_at: datetime
"""When the message was created"""

delivery_status: Literal["pending", "queued", "sent", "delivered", "failed"]
delivery_status: Literal["pending", "queued", "sent", "delivered", "received", "read", "failed"]
"""Current delivery status of a message"""

is_read: bool
"""Whether the message has been read"""
"""DEPRECATED: Use `delivery_status == "read"` instead.

Whether the message has been read.
"""

parts: List[Part]
"""Message parts in order (text, media, and link)"""
Expand Down
15 changes: 12 additions & 3 deletions src/linq/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import List, Union, Optional
from datetime import datetime
from typing_extensions import TypeAlias
from typing_extensions import Literal, TypeAlias

from pydantic import Field as FieldInfo

Expand Down Expand Up @@ -30,14 +30,23 @@ class Message(BaseModel):
created_at: datetime
"""When the message was created"""

delivery_status: Literal["pending", "queued", "sent", "delivered", "received", "read", "failed"]
"""Current delivery status of a message"""

is_delivered: bool
"""Whether the message has been delivered"""
"""
DEPRECATED: Use `delivery_status` instead (true when `delivery_status` is
`delivered` or `read`). Whether the message has been delivered.
"""

is_from_me: bool
"""Whether this message was sent by the authenticated user"""

is_read: bool
"""Whether the message has been read"""
"""DEPRECATED: Use `delivery_status == "read"` instead.

Whether the message has been read.
"""

updated_at: datetime
"""When the message was last updated"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
from .chat_typing_indicator_started_webhook_event import ChatTypingIndicatorStartedWebhookEvent
from .chat_typing_indicator_stopped_webhook_event import ChatTypingIndicatorStoppedWebhookEvent

__all__ = ["EventsWebhookEvent"]
__all__ = ["UnwrapWebhookEvent"]

EventsWebhookEvent: TypeAlias = Annotated[
UnwrapWebhookEvent: TypeAlias = Annotated[
Union[
MessageSentWebhookEvent,
MessageReceivedWebhookEvent,
Expand Down
Loading
Loading