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
30 changes: 29 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,35 @@ target-version = "py39"
[tool.ruff.lint]
extend-ignore = ["D107"]
pydocstyle = { convention = "google" }
select = ["D", "E", "F", "I", "S", "UP"]
# 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
"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
"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
"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
"RSE", # flake8-raise: enforce best practices for raise statements
"RUF", # ruff: enforce ruff specific rules
"S", # flake8-bandit: check for security issues
"SLF", # flake8-self: prevent accessing private class member
"TC", # flake8-type-checking: enforce best practices for type checking imports
"TD", # flake8-todos: check issues on TODO comment syntax
"UP", # pyupgrade: upgrade syntax for newer versions of Python
"W", # pycodestyle warnings: check for PEP 8 style convention warnings
]

[tool.ruff.lint.extend-per-file-ignores]
"examples/*.py" = ["D"]
Expand Down
18 changes: 9 additions & 9 deletions src/linkup/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from ._version import __version__
from .client import LinkupClient
from .errors import (
from ._client import LinkupClient
from ._errors import (
LinkupAuthenticationError,
LinkupFailedFetchError,
LinkupInsufficientCreditError,
Expand All @@ -9,7 +8,7 @@
LinkupTooManyRequestsError,
LinkupUnknownError,
)
from .types import (
from ._types import (
LinkupFetchResponse,
LinkupSearchImageResult,
LinkupSearchResults,
Expand All @@ -18,22 +17,23 @@
LinkupSource,
LinkupSourcedAnswer,
)
from ._version import __version__

__all__ = [
"__version__",
"LinkupClient",
"LinkupAuthenticationError",
"LinkupClient",
"LinkupFailedFetchError",
"LinkupFetchResponse",
"LinkupInsufficientCreditError",
"LinkupInvalidRequestError",
"LinkupNoResultError",
"LinkupTooManyRequestsError",
"LinkupUnknownError",
"LinkupFetchResponse",
"LinkupSearchImageResult",
"LinkupSearchResults",
"LinkupSearchStructuredResponse",
"LinkupSearchTextResult",
"LinkupSource",
"LinkupSourcedAnswer",
"LinkupTooManyRequestsError",
"LinkupUnknownError",
"__version__",
]
82 changes: 40 additions & 42 deletions src/linkup/client.py → src/linkup/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
from typing import Any, Literal, Optional, Union

import httpx
from pydantic import BaseModel
from pydantic import BaseModel, SecretStr

from linkup._version import __version__
from linkup.errors import (
from ._errors import (
LinkupAuthenticationError,
LinkupFailedFetchError,
LinkupInsufficientCreditError,
Expand All @@ -18,12 +17,13 @@
LinkupTooManyRequestsError,
LinkupUnknownError,
)
from linkup.types import (
from ._types import (
LinkupFetchResponse,
LinkupSearchResults,
LinkupSearchStructuredResponse,
LinkupSourcedAnswer,
)
from ._version import __version__


class LinkupClient:
Expand All @@ -42,16 +42,18 @@ class LinkupClient:

def __init__(
self,
api_key: Optional[str] = None,
api_key: Union[str, SecretStr, None] = None,
base_url: str = "https://api.linkup.so/v1",
) -> None:
if api_key is None:
api_key = os.getenv("LINKUP_API_KEY")
if not api_key:
raise ValueError("The Linkup API key was not provided")
if isinstance(api_key, str):
api_key = SecretStr(api_key)

self.__api_key = api_key
self.__base_url = base_url
self._api_key: SecretStr = api_key
self._base_url: str = base_url

def search(
self,
Expand Down Expand Up @@ -332,7 +334,7 @@ def _user_agent(self) -> str: # pragma: no cover

def _headers(self) -> dict[str, str]: # pragma: no cover
return {
"Authorization": f"Bearer {self.__api_key}",
"Authorization": f"Bearer {self._api_key.get_secret_value()}",
"User-Agent": self._user_agent(),
}

Expand All @@ -342,7 +344,7 @@ def _request(
url: str,
**kwargs: Any,
) -> httpx.Response: # pragma: no cover
with httpx.Client(base_url=self.__base_url, headers=self._headers()) as client:
with httpx.Client(base_url=self._base_url, headers=self._headers()) as client:
return client.request(
method=method,
url=url,
Expand All @@ -355,7 +357,7 @@ async def _async_request(
url: str,
**kwargs: Any,
) -> httpx.Response: # pragma: no cover
async with httpx.AsyncClient(base_url=self.__base_url, headers=self._headers()) as client:
async with httpx.AsyncClient(base_url=self._base_url, headers=self._headers()) as client:
return await client.request(
method=method,
url=url,
Expand Down Expand Up @@ -391,52 +393,49 @@ def _raise_linkup_error(self, response: httpx.Response) -> None:
"The provided URL might not be found or can't be fetched.\n"
f"Original error message: {error_msg}."
)
else:
raise LinkupInvalidRequestError(
"The Linkup API returned an invalid request error (400). Make sure the "
"parameters you used are valid (correct values, types, mandatory "
"parameters, etc.) and you are using the latest version of the Python "
"SDK.\n"
f"Original error message: {error_msg}."
)
elif response.status_code == 401:
raise LinkupInvalidRequestError(
"The Linkup API returned an invalid request error (400). Make sure the "
"parameters you used are valid (correct values, types, mandatory "
"parameters, etc.) and you are using the latest version of the Python "
"SDK.\n"
f"Original error message: {error_msg}."
)
if response.status_code == 401:
raise LinkupAuthenticationError(
"The Linkup API returned an authentication error (401). Make sure your API "
"key is valid.\n"
f"Original error message: {error_msg}."
)
elif response.status_code == 403:
if response.status_code == 403:
raise LinkupAuthenticationError(
"The Linkup API returned an authorization error (403). Make sure your API "
"key is valid.\n"
f"Original error message: {error_msg}."
)
elif response.status_code == 429:
if response.status_code == 429:
if code == "INSUFFICIENT_FUNDS_CREDITS":
raise LinkupInsufficientCreditError(
"The Linkup API returned an insufficient credit error (429). Make sure "
"you haven't exhausted your credits.\n"
f"Original error message: {error_msg}."
)
elif code == "TOO_MANY_REQUESTS":
if code == "TOO_MANY_REQUESTS":
raise LinkupTooManyRequestsError(
"The Linkup API returned a too many requests error (429). Make sure "
"you not sending too many requests at a time.\n"
f"Original error message: {error_msg}."
)
else:
raise LinkupUnknownError(
"The Linkup API returned an invalid request error (429). Make sure the "
"parameters you used are valid (correct values, types, mandatory "
"parameters, etc.) and you are using the latest version of the Python "
"SDK.\n"
f"Original error message: {error_msg}."
)
else:
raise LinkupUnknownError(
f"The Linkup API returned an unknown error ({response.status_code}).\n"
f"Original error message: ({error_msg})."
"The Linkup API returned an invalid request error (429). Make sure the "
"parameters you used are valid (correct values, types, mandatory "
"parameters, etc.) and you are using the latest version of the Python "
"SDK.\n"
f"Original error message: {error_msg}."
)
raise LinkupUnknownError(
f"The Linkup API returned an unknown error ({response.status_code}).\n"
f"Original error message: ({error_msg})."
)

def _get_search_params(
self,
Expand All @@ -452,11 +451,11 @@ def _get_search_params(
include_inline_citations: Optional[bool],
include_sources: Optional[bool],
) -> dict[str, Union[str, bool, list[str]]]:
params: dict[str, Union[str, bool, list[str]]] = dict(
q=query,
depth=depth,
outputType=output_type,
)
params: dict[str, Union[str, bool, list[str]]] = {
"q": query,
"depth": depth,
"outputType": output_type,
}

if structured_output_schema is not None:
if isinstance(structured_output_schema, str):
Expand Down Expand Up @@ -513,9 +512,9 @@ def _parse_search_response(
response_data: Any = response.json()
if output_type == "searchResults":
return LinkupSearchResults.model_validate(response_data)
elif output_type == "sourcedAnswer":
if output_type == "sourcedAnswer":
return LinkupSourcedAnswer.model_validate(response_data)
elif output_type == "structured":
if output_type == "structured":
if structured_output_schema is None:
raise ValueError(
"structured_output_schema must be provided when output_type is 'structured'"
Expand All @@ -535,8 +534,7 @@ def _parse_search_response(
):
return structured_output_schema.model_validate(response_data)
return response_data
else:
raise ValueError(f"Unexpected output_type value: '{output_type}'")
raise ValueError(f"Unexpected output_type value: '{output_type}'")

def _parse_fetch_response(self, response: httpx.Response) -> LinkupFetchResponse:
return LinkupFetchResponse.model_validate(response.json())
File renamed without changes.
File renamed without changes.
Loading