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
25 changes: 17 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
# Python-generated files
.idea/
# Environment variables
.env

# Reserved directories
data/
scratch/

# Python artefacts
.ipynb_checkpoints/
.venv/
.vscode/
__pycache__/
build/
dist/
wheels/
.coverage
.env
*.egg-info
*.py[oc]

# MacOS stuff
.DS_Store
# IDE artefacts
.cursor/
.idea/
.vscode/

# Coding agents stuff
# Coding agents artefacts
.claude/
.opencode/
.rtk/

playground.py
# System artefacts
.DS_Store
1 change: 1 addition & 0 deletions .rumdl.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[global]
exclude = ["scratch/**"]
line-length = 100

[MD013]
Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ When adding or changing a public API capability, update the relevant pieces toge

## Validation

Before opening a PR, run the CI checks: `make lint typecheck test`.
After having done some changes or before opening a PR, run the CI checks:
`make lint typecheck test`.

Never run the underlying checks without using these `make` commands (e.g. with `uv run ruff ...`),
as these kind of checks won't take into account some settings (like the `exclude` list).

## Non-Goals

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ target-version = "py310"
unsafe-fixes = true

[tool.ruff.lint]
exclude = ["scratch/"]
explicit-preview-rules = true
extend-ignore = ["D107"]
preview = true
Expand Down Expand Up @@ -136,7 +137,7 @@ parse_squash_commits = false

[tool.uv]
exclude-newer = "2 weeks" # Reduce risks of supply chain attacks
required-version = ">=0.11.6,<0.12.0"
required-version = ">=0.11.15,<0.12.0"

[build-system]
build-backend = "hatchling.build"
Expand Down
134 changes: 67 additions & 67 deletions src/linkup/_client.py

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions src/linkup/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class LinkupSourcedAnswer(_LinkupBaseModel):


class LinkupSearchStructuredResponse(_LinkupBaseModel):
"""A Linkup `search` structured response, with the sources supporting it.
"""A Linkup /search structured response, with the sources supporting it.

Attributes:
data: The raw structured output dictionary.
Expand Down Expand Up @@ -164,6 +164,14 @@ class LinkupSearchTaskInput(_LinkupBaseModel):
pydantic.Field(default=None, validation_alias="structuredOutputSchema")
)

@pydantic.model_validator(mode="after")
def _validate_structured_output_schema(self) -> LinkupSearchTaskInput:
if self.output_type == "structured" and self.structured_output_schema is None:
raise ValueError(
"structured_output_schema must be provided when output_type is 'structured'"
)
return self


class LinkupResearchTaskInput(_LinkupBaseModel):
"""Input for creating or retrieving a research task.
Expand Down Expand Up @@ -200,6 +208,14 @@ class LinkupResearchTaskInput(_LinkupBaseModel):
pydantic.Field(default=None, validation_alias="structuredOutputSchema")
)

@pydantic.model_validator(mode="after")
def _validate_structured_output_schema(self) -> LinkupResearchTaskInput:
if self.output_type == "structured" and self.structured_output_schema is None:
raise ValueError(
"structured_output_schema must be provided when output_type is 'structured'"
)
return self


class LinkupFetchTaskInput(_LinkupBaseModel):
"""Input for creating or retrieving a fetch task.
Expand Down Expand Up @@ -237,7 +253,7 @@ class LinkupTaskMetadata(_LinkupBaseModel):


class LinkupTaskQuota(_LinkupBaseModel):
"""Task quota information returned by `list_tasks`.
"""Task quota information returned by list_tasks.

Attributes:
in_flight: The number of tasks currently in flight.
Expand Down
17 changes: 11 additions & 6 deletions tests/unit/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ def test_search(
mock_request_response_content: bytes,
expected_search_response: Any, # noqa: ANN401
) -> None:
mocker.patch("linkup._client.date").today.return_value = date(2000, 1, 1)
request_mock = mocker.patch(
"httpx.Client.request",
return_value=Response(
Expand Down Expand Up @@ -416,7 +415,6 @@ async def test_async_search(
mock_request_response_content: bytes,
expected_search_response: Any, # noqa: ANN401
) -> None:
mocker.patch("linkup._client.date").today.return_value = date(2000, 1, 1)
request_mock = mocker.patch(
"httpx.AsyncClient.request",
return_value=Response(
Expand Down Expand Up @@ -743,7 +741,10 @@ def test_get_research_structured_output_keeps_sourced_answer_shape_raw(
"id": "bfeb26f5-f4d6-47d2-9818-7f62fbcd0b0c",
"input": {
"outputType": "structured",
"q": "query"
"q": "query",
"structuredOutputSchema": {
"type": "object"
}
},
"output": {
"answer": "structured answer field",
Expand All @@ -763,6 +764,7 @@ def test_get_research_structured_output_keeps_sourced_answer_shape_raw(
"answer": "structured answer field",
"sources": [],
}
assert research_response.input.structured_output_schema == {"type": "object"}


@pytest.mark.asyncio
Expand Down Expand Up @@ -1527,7 +1529,7 @@ def test_get_task_structured_search_output_keeps_search_results_shape_raw(
assert task.output == {"results": []}


def test_get_task_structured_search_output_without_schema_raw(
def test_get_task_structured_search_output_raw(
mocker: MockerFixture, client: linkup.Client
) -> None:
mocker.patch(
Expand All @@ -1542,7 +1544,10 @@ def test_get_task_structured_search_output_without_schema_raw(
"input": {
"depth": "standard",
"outputType": "structured",
"q": "query"
"q": "query",
"structuredOutputSchema": {
"type": "object"
}
},
"output": {
"summary": "done"
Expand All @@ -1558,7 +1563,7 @@ def test_get_task_structured_search_output_without_schema_raw(
task = client.get_task("bfeb26f5-f4d6-47d2-9818-7f62fbcd0b0c")

assert isinstance(task, linkup.SearchTask)
assert task.input.structured_output_schema is None
assert task.input.structured_output_schema == {"type": "object"}
assert task.output == {"summary": "done"}


Expand Down
34 changes: 34 additions & 0 deletions tests/unit/types_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Any

import pydantic
import pytest

import linkup


@pytest.mark.parametrize(
"task_input",
[
pytest.param(
lambda: linkup.SearchTaskInput(
query="query",
depth="standard",
output_type="structured",
),
id="search",
),
pytest.param(
lambda: linkup.ResearchTaskInput(
query="query",
output_type="structured",
),
id="research",
),
],
)
def test_structured_task_input_requires_schema(task_input: Any) -> None: # noqa: ANN401
with pytest.raises(
pydantic.ValidationError,
match="structured_output_schema must be provided",
):
task_input()
Loading
Loading