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
7 changes: 4 additions & 3 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<!-- Make sure all items below are checked before submitting the Pull Request. -->

- [ ] I have installed pre-commit on this project (for instance with the `make install-dev` command)
**before** creating any commit, or I have run successfully the `make lint` command on my changes
- [ ] I have run successfully the `make test` command on my changes
- [ ] I have updated the `README.md` if my changes affected it
**before** creating any commit, or I have run successfully the `make format-lint` command on my
changes.
- [ ] I have run successfully the `make test` command on my changes.
- [ ] I have updated the `README.md` if my changes affected it.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ install-dev:
@$(MAKE) install
uv run pre-commit install

lint:
format-lint:
SKIP=no-commit-to-branch uv run pre-commit run --all-files
format-lint-unsafe:
uv run --with ruff ruff check --fix --unsafe-fixes .
@echo
@$(MAKE) format-lint

test-mypy:
@# Avoid running mypy on the whole directory ("./") to avoid potential conflicts with files with the same name (e.g. between different types of tests)
Expand Down
4 changes: 2 additions & 2 deletions examples/1_search_results_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
fill the missing values, or pass a Linkup API key to the `LinkupClient` initialization.
"""

import rich
from dotenv import load_dotenv
from rich import print

from linkup import LinkupClient

Expand All @@ -20,4 +20,4 @@
depth="standard", # or "deep"
output_type="searchResults",
)
print(response)
rich.print(response)
4 changes: 2 additions & 2 deletions examples/2_sourced_answer_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
fill the missing values, or pass a Linkup API key to the `LinkupClient` initialization.
"""

import rich
from dotenv import load_dotenv
from rich import print

from linkup import LinkupClient

Expand All @@ -22,4 +22,4 @@
output_type="sourcedAnswer",
include_inline_citations=False,
)
print(response)
rich.print(response)
4 changes: 2 additions & 2 deletions examples/3_structured_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
fill the missing values, or pass a Linkup API key to the `LinkupClient` initialization.
"""

import rich
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from rich import print

from linkup import LinkupClient

Expand All @@ -34,4 +34,4 @@ class Events(BaseModel):
structured_output_schema=Events, # or json.dumps(Events.model_json_schema())
include_sources=False,
)
print(response)
rich.print(response)
4 changes: 2 additions & 2 deletions examples/4_asynchronous_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import asyncio
import time

import rich
from dotenv import load_dotenv
from rich import print

from linkup import LinkupClient

Expand All @@ -35,7 +35,7 @@ async def search(idx: int, query: str) -> None:
output_type="searchResults", # or "sourcedAnswer" or "structured"
)
print(f"{idx + 1}: {time.time() - t0:.3f}s")
print(response)
rich.print(response)
print("-" * 100)


Expand Down
4 changes: 2 additions & 2 deletions examples/5_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
fill the missing values, or pass a Linkup API key to the `LinkupClient` initialization.
"""

import rich
from dotenv import load_dotenv
from rich import print

from linkup import LinkupClient

Expand All @@ -17,4 +17,4 @@
response = client.fetch(
url="https://docs.linkup.so",
)
print(response)
rich.print(response)
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,22 @@ target-version = "py39"
[tool.ruff.lint]
extend-ignore = ["D107"]
pydocstyle = { convention = "google" }
# TODO: enable commented out rules and fix errors
select = [
# "A", # flake8-builtins: avoid shadowing built-in names
# "ANN", # flake8-annotations: check for missing type annotations
"A", # flake8-builtins: avoid shadowing built-in names
"ANN", # flake8-annotations: check for missing type annotations
"ASYNC", # flake8-async: enforce best practices for async code
"B", # flake8-bugbear: find likely bugs and design problems in your program
"C4", # flake8-comprehensions: enforce best practices for list/set/dict comprehensions
"D", # pydocstyle: check compliance with docstring conventions
"E", # pycodestyle errors: check for PEP 8 style convention errors
"F", # pyflakes: check for Python source file errors
# "FA", # flake8-future-annotations: enforce usage of future annotations when relevant
"FA", # flake8-future-annotations: enforce usage of future annotations when relevant
"I", # isort: enforce import sorting
"ICN", # flake8-import-conventions: enforce general import conventions
"ISC", # flake8-implicit-str-concat: check for invalid implicit or explicit string concatenation
"N", # pep8-naming: check for naming convention violations
"PERF", # perflint: check for performance anti-patterns
# "PT", # flake8-pytest-style: check common style issues and inconsistencies in pytest-based tests
"PT", # flake8-pytest-style: check common style issues and inconsistencies in pytest-based tests
"PTH", # flake8-use-pathlib: enforce usage of pathlib for path manipulations instead of os.path
"Q", # flake8-quotes: enforce consistent string quote usage
"RET", # flake8-return: enforce best practices for return statements
Expand Down
113 changes: 62 additions & 51 deletions src/linkup/_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Linkup client, the entrypoint for Linkup functions."""

from __future__ import annotations

import json
import os
from datetime import date
from typing import Any, Literal, Optional, Union
from datetime import date # noqa: TC003 (`date` is used in test mocks)
from typing import Any, Literal

import httpx
from pydantic import BaseModel, SecretStr
Expand Down Expand Up @@ -42,7 +44,7 @@ class LinkupClient:

def __init__(
self,
api_key: Union[str, SecretStr, None] = None,
api_key: str | SecretStr | None = None,
base_url: str = "https://api.linkup.so/v1",
) -> None:
if api_key is None:
Expand All @@ -60,15 +62,16 @@ def search(
query: str,
depth: Literal["standard", "deep"],
output_type: Literal["searchResults", "sourcedAnswer", "structured"],
structured_output_schema: Union[type[BaseModel], str, None] = None,
include_images: Optional[bool] = None,
from_date: Optional[date] = None,
to_date: Optional[date] = None,
exclude_domains: Optional[list[str]] = None,
include_domains: Optional[list[str]] = None,
include_inline_citations: Optional[bool] = None,
include_sources: Optional[bool] = None,
) -> Any:
structured_output_schema: type[BaseModel] | str | None = None,
include_images: bool | None = None,
from_date: date | None = None,
to_date: date | None = None,
exclude_domains: list[str] | None = None,
include_domains: list[str] | None = None,
max_results: int | None = None,
include_inline_citations: bool | None = None,
include_sources: bool | None = None,
) -> Any: # noqa: ANN401
"""Perform a web search using the Linkup API `search` endpoint.

All optional parameters will default to the Linkup API defaults when not provided. The
Expand All @@ -93,6 +96,7 @@ def search(
search results will not be filtered by date.
exclude_domains: If you want to exclude specific domains from your search.
include_domains: If you want the search to only return results from certain domains.
max_results: The maximum number of results to return.
include_inline_citations: If output_type is "sourcedAnswer", indicate whether the
answer should include inline citations.
include_sources: If output_type is "structured", indicate whether the answer should
Expand All @@ -117,7 +121,7 @@ def search(
LinkupInsufficientCreditError: If you have run out of credit.
LinkupNoResultError: If the search query did not yield any result.
"""
params: dict[str, Union[str, bool, list[str]]] = self._get_search_params(
params: dict[str, str | bool | int | list[str]] = self._get_search_params(
query=query,
depth=depth,
output_type=output_type,
Expand All @@ -127,6 +131,7 @@ def search(
to_date=to_date,
exclude_domains=exclude_domains,
include_domains=include_domains,
max_results=max_results,
include_inline_citations=include_inline_citations,
include_sources=include_sources,
)
Expand All @@ -152,15 +157,16 @@ async def async_search(
query: str,
depth: Literal["standard", "deep"],
output_type: Literal["searchResults", "sourcedAnswer", "structured"],
structured_output_schema: Union[type[BaseModel], str, None] = None,
include_images: Optional[bool] = None,
from_date: Optional[date] = None,
to_date: Optional[date] = None,
exclude_domains: Optional[list[str]] = None,
include_domains: Optional[list[str]] = None,
include_inline_citations: Optional[bool] = None,
include_sources: Optional[bool] = None,
) -> Any:
structured_output_schema: type[BaseModel] | str | None = None,
include_images: bool | None = None,
from_date: date | None = None,
to_date: date | None = None,
exclude_domains: list[str] | None = None,
include_domains: list[str] | None = None,
max_results: int | None = None,
include_inline_citations: bool | None = None,
include_sources: bool | None = None,
) -> Any: # noqa: ANN401
"""Asynchronously perform a web search using the Linkup API `search` endpoint.

All optional parameters will default to the Linkup API defaults when not provided. The
Expand All @@ -185,6 +191,7 @@ async def async_search(
search results will not be filtered by date.
exclude_domains: If you want to exclude specific domains from your search.
include_domains: If you want the search to only return results from certain domains.
max_results: The maximum number of results to return.
include_inline_citations: If output_type is "sourcedAnswer", indicate whether the
answer should include inline citations.
include_sources: If output_type is "structured", indicate whether the answer should
Expand All @@ -209,7 +216,7 @@ async def async_search(
LinkupInsufficientCreditError: If you have run out of credit.
LinkupNoResultError: If the search query did not yield any result.
"""
params: dict[str, Union[str, bool, list[str]]] = self._get_search_params(
params: dict[str, str | bool | int | list[str]] = self._get_search_params(
query=query,
depth=depth,
output_type=output_type,
Expand All @@ -219,6 +226,7 @@ async def async_search(
to_date=to_date,
exclude_domains=exclude_domains,
include_domains=include_domains,
max_results=max_results,
include_inline_citations=include_inline_citations,
include_sources=include_sources,
)
Expand All @@ -242,9 +250,9 @@ async def async_search(
def fetch(
self,
url: str,
include_raw_html: Optional[bool] = None,
render_js: Optional[bool] = None,
extract_images: Optional[bool] = None,
include_raw_html: bool | None = None,
render_js: bool | None = None,
extract_images: bool | None = None,
) -> LinkupFetchResponse:
"""Fetch the content of a web page using the Linkup API `fetch` endpoint.

Expand All @@ -266,7 +274,7 @@ def fetch(
LinkupInvalidRequestError: If the provided URL is not valid.
LinkupFailedFetchError: If the provided URL is not found or can't be fetched.
"""
params: dict[str, Union[str, bool]] = self._get_fetch_params(
params: dict[str, str | bool] = self._get_fetch_params(
url=url,
include_raw_html=include_raw_html,
render_js=render_js,
Expand All @@ -287,9 +295,9 @@ def fetch(
async def async_fetch(
self,
url: str,
include_raw_html: Optional[bool] = None,
render_js: Optional[bool] = None,
extract_images: Optional[bool] = None,
include_raw_html: bool | None = None,
render_js: bool | None = None,
extract_images: bool | None = None,
) -> LinkupFetchResponse:
"""Asynchronously fetch the content of a web page using the Linkup API `fetch` endpoint.

Expand All @@ -311,7 +319,7 @@ async def async_fetch(
LinkupInvalidRequestError: If the provided URL is not valid.
LinkupFailedFetchError: If the provided URL is not found or can't be fetched.
"""
params: dict[str, Union[str, bool]] = self._get_fetch_params(
params: dict[str, str | bool] = self._get_fetch_params(
url=url,
include_raw_html=include_raw_html,
render_js=render_js,
Expand Down Expand Up @@ -342,7 +350,7 @@ def _request(
self,
method: str,
url: str,
**kwargs: Any,
**kwargs: Any, # noqa: ANN401
) -> httpx.Response: # pragma: no cover
with httpx.Client(base_url=self._base_url, headers=self._headers()) as client:
return client.request(
Expand All @@ -355,7 +363,7 @@ async def _async_request(
self,
method: str,
url: str,
**kwargs: Any,
**kwargs: Any, # noqa: ANN401
) -> httpx.Response: # pragma: no cover
async with httpx.AsyncClient(base_url=self._base_url, headers=self._headers()) as client:
return await client.request(
Expand Down Expand Up @@ -442,16 +450,17 @@ def _get_search_params(
query: str,
depth: Literal["standard", "deep"],
output_type: Literal["searchResults", "sourcedAnswer", "structured"],
structured_output_schema: Union[type[BaseModel], str, None],
include_images: Optional[bool],
from_date: Optional[date],
to_date: Optional[date],
exclude_domains: Optional[list[str]],
include_domains: Optional[list[str]],
include_inline_citations: Optional[bool],
include_sources: Optional[bool],
) -> dict[str, Union[str, bool, list[str]]]:
params: dict[str, Union[str, bool, list[str]]] = {
structured_output_schema: type[BaseModel] | str | None,
include_images: bool | None,
from_date: date | None,
to_date: date | None,
exclude_domains: list[str] | None,
include_domains: list[str] | None,
max_results: int | None,
include_inline_citations: bool | None,
include_sources: bool | None,
) -> dict[str, str | bool | int | list[str]]:
params: dict[str, str | bool | int | list[str]] = {
"q": query,
"depth": depth,
"outputType": output_type,
Expand All @@ -477,6 +486,8 @@ def _get_search_params(
params["excludeDomains"] = exclude_domains
if include_domains is not None:
params["includeDomains"] = include_domains
if max_results is not None:
params["maxResults"] = max_results
if include_inline_citations is not None:
params["includeInlineCitations"] = include_inline_citations
if include_sources is not None:
Expand All @@ -487,11 +498,11 @@ def _get_search_params(
def _get_fetch_params(
self,
url: str,
include_raw_html: Optional[bool],
render_js: Optional[bool],
extract_images: Optional[bool],
) -> dict[str, Union[str, bool]]:
params: dict[str, Union[str, bool]] = {
include_raw_html: bool | None,
render_js: bool | None,
extract_images: bool | None,
) -> dict[str, str | bool]:
params: dict[str, str | bool] = {
"url": url,
}
if include_raw_html is not None:
Expand All @@ -506,9 +517,9 @@ def _parse_search_response(
self,
response: httpx.Response,
output_type: Literal["searchResults", "sourcedAnswer", "structured"],
structured_output_schema: Union[type[BaseModel], str, None],
include_sources: Optional[bool],
) -> Any:
structured_output_schema: type[BaseModel] | str | None,
include_sources: bool | None,
) -> Any: # noqa: ANN401
response_data: Any = response.json()
if output_type == "searchResults":
return LinkupSearchResults.model_validate(response_data)
Expand Down
2 changes: 2 additions & 0 deletions src/linkup/_types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Input and output types for Linkup functions."""

# ruff: noqa: FA100 (pydantic models don't play well with future annotations)

from typing import Any, Literal, Optional, Union

from pydantic import BaseModel, ConfigDict, Field
Expand Down
Loading