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
4 changes: 4 additions & 0 deletions src/linkup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
LinkupTimeoutError,
LinkupTooManyRequestsError,
LinkupUnknownError,
LinkupUnsupportedTaskTypeError,
)
from ._types import (
JSONObject,
Expand Down Expand Up @@ -78,6 +79,7 @@
TimeoutError = LinkupTimeoutError # noqa: A001
TooManyRequestsError = LinkupTooManyRequestsError
UnknownError = LinkupUnknownError
UnsupportedTaskTypeError = LinkupUnsupportedTaskTypeError

__all__ = [
"AuthenticationError",
Expand Down Expand Up @@ -130,6 +132,7 @@
"LinkupTimeoutError",
"LinkupTooManyRequestsError",
"LinkupUnknownError",
"LinkupUnsupportedTaskTypeError",
"NoResultError",
"PaymentRequiredError",
"ResearchTask",
Expand All @@ -153,5 +156,6 @@
"TimeoutError",
"TooManyRequestsError",
"UnknownError",
"UnsupportedTaskTypeError",
"__version__",
]
10 changes: 9 additions & 1 deletion src/linkup/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
LinkupTimeoutError,
LinkupTooManyRequestsError,
LinkupUnknownError,
LinkupUnsupportedTaskTypeError,
)
from ._types import (
JSONObject,
Expand Down Expand Up @@ -1341,6 +1342,11 @@ def _raise_linkup_error(self, response: httpx.Response) -> None:
f"Original error message: {error_msg}."
)
if response.status_code == 403:
if code == "TASK_TYPE_NOT_SUPPORTED":
raise LinkupUnsupportedTaskTypeError(
"The Linkup API returned an unsupported task type error (403). \n"
f"Original error message: {error_msg}."
)
raise LinkupAuthenticationError(
"The Linkup API returned an authorization error (403). Make sure your API "
"key is valid.\n"
Expand Down Expand Up @@ -1692,7 +1698,9 @@ def _parse_task(self, task_data: dict[str, Any]) -> LinkupTask:
if task_type == "research":
return self._parse_research_task(task_data)

raise ValueError(f"Unexpected task type value: '{task_type}'")
raise LinkupUnsupportedTaskTypeError(
f"The Linkup API returned an unsupported task type '{task_type}'."
)

def _parse_research_tasks_page(self, response_data: dict[str, Any]) -> LinkupResearchTasksPage:
return LinkupResearchTasksPage.model_validate(
Expand Down
6 changes: 6 additions & 0 deletions src/linkup/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,9 @@ class LinkupUnknownError(Exception):
"""Unknown error, raised when the Linkup API returns an unknown status code."""

pass


class LinkupUnsupportedTaskTypeError(Exception):
"""Unsupported task type error, raised when a task type is not supported by this SDK."""

pass
43 changes: 43 additions & 0 deletions tests/unit/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,19 @@ async def test_async_search_structured_output_requires_schema(client: linkup.Cli
""",
linkup.AuthenticationError,
),
(
403,
b"""
{
"error": {
"code": "TASK_TYPE_NOT_SUPPORTED",
"message": "Extract tasks are not enabled for this organization.",
"details": []
}
}
""",
linkup.UnsupportedTaskTypeError,
),
(
401,
b"""
Expand Down Expand Up @@ -1492,6 +1505,36 @@ def test_list_tasks(mocker: MockerFixture, client: linkup.Client) -> None:
assert tasks_page.quota.in_flight == 1


def test_get_task_unsupported_task_type(mocker: MockerFixture, client: linkup.Client) -> None:
mocker.patch(
"httpx.Client.request",
return_value=Response(
status_code=200,
content=b"""
{
"createdAt": "2026-05-18T00:00:00.000Z",
"error": null,
"id": "extract-task",
"input": {
"q": "query",
"url": "https://example.com"
},
"output": null,
"status": "pending",
"type": "extract",
"updatedAt": "2026-05-18T00:00:00.000Z"
}
""",
),
)

with pytest.raises(
linkup.UnsupportedTaskTypeError,
match="unsupported task type 'extract'",
):
client.get_task("extract-task")


def test_get_task_structured_search_output_keeps_search_results_shape_raw(
mocker: MockerFixture, client: linkup.Client
) -> None:
Expand Down
Loading