From a233e4ad403d99e744b6730a34d167719667f6ff Mon Sep 17 00:00:00 2001 From: cjumel Date: Mon, 15 Sep 2025 18:33:03 +0200 Subject: [PATCH 1/7] chore: use lowest supported Python version --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index c8cfe39..bd28b9c 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10 +3.9 From 8062289d9cd508e787662a357cb69dee87f61169 Mon Sep 17 00:00:00 2001 From: cjumel Date: Mon, 15 Sep 2025 18:36:57 +0200 Subject: [PATCH 2/7] chore: standardize tools and configs This commit makes sure we use the same tools and same configs as in our other internal repositories. --- .editorconfig | 29 ++++ .gitignore | 170 +--------------------- .mdformat.toml | 2 + .pre-commit-config.yaml | 26 +++- .taplo.toml | 7 + .yamlfmt.yaml | 4 + Makefile | 34 ++++- pyproject.toml | 19 ++- tests/{unit_tests => unit}/client_test.py | 0 tests/{unit_tests => unit}/conftest.py | 0 uv.lock | 14 -- 11 files changed, 107 insertions(+), 198 deletions(-) create mode 100644 .editorconfig create mode 100644 .mdformat.toml create mode 100644 .taplo.toml create mode 100644 .yamlfmt.yaml rename tests/{unit_tests => unit}/client_test.py (100%) rename tests/{unit_tests => unit}/conftest.py (100%) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fa24419 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +root = true + +[*] +charset = utf-8 +insert_final_newline = true + +[*.md] +indent_style = space +indent_size = 2 +max_line_length = 100 + +[*.py] +indent_style = space +indent_size = 4 +max_line_length = 100 + +[*.toml] +indent_style = space +indent_size = 2 +max_line_length = 100 + +[*.yaml] +indent_style = space +indent_size = 2 +max_line_length = 100 + +[Makefile] +indent_style = tab +indent_size = 8 diff --git a/.gitignore b/.gitignore index 0fd741c..47f879f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,170 +1,16 @@ -# Byte-compiled / optimized / DLL files +# Python-generated files +.idea/ +.ipynb_checkpoints/ +.venv/ +.vscode/ __pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python build/ -develop-eggs/ dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ .coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments .env -.venv* -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site +*.egg-info +*.py[oc] -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - - -# OSX +# MacOS stuff .DS_Store - -# Dev -playground.ipynb -playground.py diff --git a/.mdformat.toml b/.mdformat.toml new file mode 100644 index 0000000..dbe02d6 --- /dev/null +++ b/.mdformat.toml @@ -0,0 +1,2 @@ +number = true +wrap = 100 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97cfbe5..a9c4758 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,39 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-merge-conflict - id: detect-private-key - id: end-of-file-fixer - - id: trailing-whitespace + - id: name-tests-test + - id: no-commit-to-branch + args: [--branch, main] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 + rev: v0.13.0 hooks: - id: ruff args: [--fix] - id: ruff-format + - repo: https://github.com/hukkin/mdformat + rev: 0.7.22 + hooks: + - id: mdformat + exclude: CHANGELOG.md + + - repo: https://github.com/ComPWA/taplo-pre-commit + rev: v0.9.3 + hooks: + - id: taplo-format + + - repo: https://github.com/google/yamlfmt + rev: v0.17.2 + hooks: + - id: yamlfmt + - repo: https://github.com/gitleaks/gitleaks - rev: v8.21.2 + rev: v8.28.0 hooks: - id: gitleaks diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..e48c956 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,7 @@ +[formatting] +align_comments = false +allowed_blank_lines = 1 +column_width = 100 +reorder_arrays = true +reorder_inline_tables = true +reorder_keys = true diff --git a/.yamlfmt.yaml b/.yamlfmt.yaml new file mode 100644 index 0000000..8b98c72 --- /dev/null +++ b/.yamlfmt.yaml @@ -0,0 +1,4 @@ +formatter: + type: basic + retain_line_breaks_single: true + max_line_length: 100 diff --git a/Makefile b/Makefile index b36c26c..e01d9c0 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,30 @@ install-dev: @$(MAKE) install uv run pre-commit install +lint: + SKIP=no-commit-to-branch uv run pre-commit run --all-files + +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) + uv run mypy ./src/ + uv run mypy ./tests/unit/ +test-pytest: + uv run pytest --cov=src/linkup/ ./tests/unit/ test: - @echo "Running tests..." - uv run pre-commit run --all-files - uv run mypy . - # Follow the test practices recommanded by LangChain (v0.3) - # See https://python.langchain.com/docs/contributing/how_to/integrations/standard_tests/ - uv run pytest --cov=src/linkup/ --cov-report term-missing --disable-socket --allow-unix-socket tests/unit_tests - # TODO: uncomment the following line when integration tests are ready - # pytest tests/integration_tests + @$(MAKE) test-mypy + @echo + @$(MAKE) test-pytest + +update-uv: + uv lock --upgrade + uv sync +update-pre-commit: + uv run pre-commit autoupdate + +clean: + rm -rf dist/ + rm -f .coverage + rm -rf .mypy_cache/ + rm -rf .pytest_cache/ + rm -rf .ruff_cache/ + rm -rf **/*/__pycache__/ diff --git a/pyproject.toml b/pyproject.toml index c1130c3..6b1597c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,33 +39,29 @@ dev = [ "pytest-asyncio>=1.0.0", "pytest-cov>=6.2.1", "pytest-mock>=3.14.1", - "pytest-socket>=0.7.0", ] [tool.mypy] -exclude = ['^tests/', 'venv/', '.venv/'] strict = true +warn_unreachable = true [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" [tool.coverage.report] -exclude_also = ["raise ValueError", "raise TypeError"] +exclude_also = ["raise TypeError", "raise ValueError"] +show_missing = true +skip_covered = true [tool.ruff] line-length = 100 target-version = "py39" [tool.ruff.lint] -select = [ - "E", # pycodestyle - "F", # pyflakes - "I", # isort - "S", # flake8-bandit -] +select = ["E", "F", "I", "S", "UP"] [tool.ruff.lint.extend-per-file-ignores] -"tests/**/*_test.py" = ["S101"] # Use of assert detected +"tests/**/*_test.py" = ["S101"] [build-system] requires = ["hatchling"] @@ -88,3 +84,6 @@ version_toml = ["pyproject.toml:project.version"] [tool.semantic_release.commit_parser_options] parse_squash_commits = false + +[tool.uv] +required-version = ">=0.8.0,<0.9.0" diff --git a/tests/unit_tests/client_test.py b/tests/unit/client_test.py similarity index 100% rename from tests/unit_tests/client_test.py rename to tests/unit/client_test.py diff --git a/tests/unit_tests/conftest.py b/tests/unit/conftest.py similarity index 100% rename from tests/unit_tests/conftest.py rename to tests/unit/conftest.py diff --git a/uv.lock b/uv.lock index 290a418..95ae93b 100644 --- a/uv.lock +++ b/uv.lock @@ -257,7 +257,6 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-mock" }, - { name = "pytest-socket" }, ] [package.metadata] @@ -276,7 +275,6 @@ dev = [ { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, { name = "pytest-mock", specifier = ">=3.14.1" }, - { name = "pytest-socket", specifier = ">=0.7.0" }, ] [[package]] @@ -585,18 +583,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, ] -[[package]] -name = "pytest-socket" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/ff/90c7e1e746baf3d62ce864c479fd53410b534818b9437413903596f81580/pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3", size = 12389, upload-time = "2024-01-28T20:17:23.177Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754, upload-time = "2024-01-28T20:17:22.105Z" }, -] - [[package]] name = "pyyaml" version = "6.0.2" From c6dae6e6398234eab48f9bdba77f8c3307bbc176 Mon Sep 17 00:00:00 2001 From: cjumel Date: Mon, 15 Sep 2025 18:50:22 +0200 Subject: [PATCH 3/7] chore: format and lint --- .github/pull_request_template.md | 3 +- README.md | 1 - examples/3_structured_search.py | 4 +- examples/4_asynchronous_search.py | 5 +- pyproject.toml | 31 +++--- src/linkup/client.py | 24 ++-- src/linkup/types.py | 10 +- tests/unit/client_test.py | 177 ++++++++++++++---------------- 8 files changed, 115 insertions(+), 140 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 40d29c2..d1b46f0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,4 +8,5 @@ - [ ] I have run the tests locally with `make install-dev` and `make test` - [ ] I have updated the `README.md` if my changes affected it -- [ ] I have updated the package version in `linkup/_version.py` and `pyproject.toml` if I plan on creating a new release +- [ ] I have updated the package version in `linkup/_version.py` and `pyproject.toml` if I plan on + creating a new release diff --git a/README.md b/README.md index 798d6a5..798f73b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ ![PyPI - Downloads](https://img.shields.io/pypi/dm/linkup-sdk) [![Discord](https://img.shields.io/discord/1303713168916348959?color=7289da&logo=discord&logoColor=white)](https://discord.gg/9q9mCYJa86) - A [Python SDK](https://docs.linkup.so/pages/sdk/python/python) for the [Linkup API](https://www.linkup.so/), allowing easy integration with Linkup's services. 🐍 diff --git a/examples/3_structured_search.py b/examples/3_structured_search.py index 14a5425..da12a8b 100644 --- a/examples/3_structured_search.py +++ b/examples/3_structured_search.py @@ -4,8 +4,6 @@ documented schema to steer the Linkup search in any direction. """ -from typing import List - from pydantic import BaseModel, Field from linkup import LinkupClient @@ -17,7 +15,7 @@ class Event(BaseModel): class Events(BaseModel): - events: List[Event] = Field(description="The list of events") + events: list[Event] = Field(description="The list of events") client = LinkupClient() diff --git a/examples/4_asynchronous_search.py b/examples/4_asynchronous_search.py index 22fa949..99be9b2 100644 --- a/examples/4_asynchronous_search.py +++ b/examples/4_asynchronous_search.py @@ -6,13 +6,12 @@ import asyncio import time -from typing import List from linkup import LinkupClient client = LinkupClient() -queries: List[str] = [ +queries: list[str] = [ "What are the 3 major events in the life of Abraham Lincoln?", "What are the 3 major events in the life of George Washington?", ] @@ -27,7 +26,7 @@ async def search(idx: int, query: str) -> None: depth="standard", # or "deep" output_type="searchResults", # or "sourcedAnswer" or "structured" ) - print(f"{idx+1}: {time.time() - t0:.3f}s") + print(f"{idx + 1}: {time.time() - t0:.3f}s") print(response) print("-" * 100) diff --git a/pyproject.toml b/pyproject.toml index 6b1597c..1ceb9be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,33 +1,28 @@ [project] -name = "linkup-sdk" -version = "0.2.9" +authors = [{ email = "contact@linkup.so", name = "LINKUP TECHNOLOGIES" }] description = "A Python Client SDK for the Linkup API" +keywords = ["api", "client", "linkup", "sdk", "search"] +license = "MIT" +name = "linkup-sdk" readme = "README.md" requires-python = ">=3.9" -authors = [ - { name = "LINKUP TECHNOLOGIES", email = "contact@linkup.so" } -] -keywords = ["linkup", "api", "sdk", "client", "search"] -license = "MIT" +version = "0.2.9" classifiers = [ "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent" + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", ] -dependencies = [ - "httpx", - "pydantic" -] +dependencies = ["httpx", "pydantic"] [project.optional-dependencies] build = ["uv>=0.8.0,<0.9.0"] # For python-semantic-release build command, used in GitHub actions [project.urls] -Homepage = "https://github.com/LinkupPlatform/linkup-python-sdk" Documentation = "https://github.com/LinkupPlatform/linkup-python-sdk#readme" +Homepage = "https://github.com/LinkupPlatform/linkup-python-sdk" Source = "https://github.com/LinkupPlatform/linkup-python-sdk" Tracker = "https://github.com/LinkupPlatform/linkup-python-sdk/issues" @@ -35,10 +30,10 @@ Tracker = "https://github.com/LinkupPlatform/linkup-python-sdk/issues" dev = [ "mypy>=1.16.1", "pre-commit>=4.2.0", - "pytest>=8.4.1", "pytest-asyncio>=1.0.0", "pytest-cov>=6.2.1", "pytest-mock>=3.14.1", + "pytest>=8.4.1", ] [tool.mypy] @@ -64,22 +59,22 @@ select = ["E", "F", "I", "S", "UP"] "tests/**/*_test.py" = ["S101"] [build-system] -requires = ["hatchling"] build-backend = "hatchling.build" +requires = ["hatchling"] [tool.hatch.build.targets.wheel] packages = ["src/linkup"] # Because project and source code directory names differ [tool.semantic_release] -commit_message = "chore: release v{version}\n\nAutomatically generated by python-semantic-release\n\n[skip ci]" # [skip ci] is needed to recursively calling the release CI allow_zero_version = true -major_on_zero = false build_command = """ python -m pip install -e '.[build]' uv lock --upgrade-package "$PACKAGE_NAME" git add uv.lock uv build """ +commit_message = "chore: release v{version}\n\nAutomatically generated by python-semantic-release\n\n[skip ci]" # [skip ci] is needed to recursively calling the release CI +major_on_zero = false version_toml = ["pyproject.toml:project.version"] [tool.semantic_release.commit_parser_options] diff --git a/src/linkup/client.py b/src/linkup/client.py index 7aaacab..3fe9195 100644 --- a/src/linkup/client.py +++ b/src/linkup/client.py @@ -1,7 +1,7 @@ import json import os from datetime import date -from typing import Any, Dict, Literal, Optional, Type, Union +from typing import Any, Literal, Optional, Union import httpx from pydantic import BaseModel @@ -51,7 +51,7 @@ def search( query: str, depth: Literal["standard", "deep"], output_type: Literal["searchResults", "sourcedAnswer", "structured"], - structured_output_schema: Union[Type[BaseModel], str, None] = None, + structured_output_schema: Union[type[BaseModel], str, None] = None, include_images: bool = False, exclude_domains: Union[list[str], None] = None, include_domains: Union[list[str], None] = None, @@ -97,7 +97,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, Union[str, bool, list[str]]] = self._get_search_params( query=query, depth=depth, output_type=output_type, @@ -129,7 +129,7 @@ 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, + structured_output_schema: Union[type[BaseModel], str, None] = None, include_images: bool = False, exclude_domains: Union[list[str], None] = None, include_domains: Union[list[str], None] = None, @@ -174,7 +174,7 @@ async def async_search( LinkupAuthenticationError: If the Linkup API key is invalid, or there is no more credit available. """ - params: Dict[str, Union[str, bool, list[str]]] = self._get_search_params( + params: dict[str, Union[str, bool, list[str]]] = self._get_search_params( query=query, depth=depth, output_type=output_type, @@ -204,7 +204,7 @@ async def async_search( def _user_agent(self) -> str: # pragma: no cover return f"Linkup-Python/{self.__version__}" - def _headers(self) -> Dict[str, str]: # pragma: no cover + def _headers(self) -> dict[str, str]: # pragma: no cover return { "Authorization": f"Bearer {self.__api_key}", "User-Agent": self._user_agent(), @@ -312,14 +312,14 @@ def _get_search_params( query: str, depth: Literal["standard", "deep"], output_type: Literal["searchResults", "sourcedAnswer", "structured"], - structured_output_schema: Union[Type[BaseModel], str, None], + structured_output_schema: Union[type[BaseModel], str, None], include_images: bool, from_date: Union[date, None], include_domains: Union[list[str], None], exclude_domains: Union[list[str], None], to_date: Union[date, None], - ) -> Dict[str, Union[str, bool, list[str]]]: - params: Dict[str, Union[str, bool, list[str]]] = dict( + ) -> dict[str, Union[str, bool, list[str]]]: + params: dict[str, Union[str, bool, list[str]]] = dict( q=query, depth=depth, outputType=output_type, @@ -330,7 +330,7 @@ def _get_search_params( if isinstance(structured_output_schema, str): params["structuredOutputSchema"] = structured_output_schema elif issubclass(structured_output_schema, BaseModel): - json_schema: Dict[str, Any] = structured_output_schema.model_json_schema() + json_schema: dict[str, Any] = structured_output_schema.model_json_schema() params["structuredOutputSchema"] = json.dumps(json_schema) else: raise TypeError( @@ -351,10 +351,10 @@ def _validate_search_response( self, response: httpx.Response, output_type: Literal["searchResults", "sourcedAnswer", "structured"], - structured_output_schema: Union[Type[BaseModel], str, None], + structured_output_schema: Union[type[BaseModel], str, None], ) -> Any: response_data: Any = response.json() - output_base_model: Optional[Type[BaseModel]] = None + output_base_model: Optional[type[BaseModel]] = None if output_type == "searchResults": output_base_model = LinkupSearchResults elif output_type == "sourcedAnswer": diff --git a/src/linkup/types.py b/src/linkup/types.py index 4e7d7d6..3d6128c 100644 --- a/src/linkup/types.py +++ b/src/linkup/types.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import Union from pydantic import BaseModel @@ -8,7 +8,7 @@ class LinkupSearchTextResult(BaseModel): A text result from a Linkup search. Attributes: - type: The type of the search result, in this case 'text' + type: The type of the search result, in this case "text". name: The name of the search result. url: The URL of the search result. content: The text of the search result. @@ -25,7 +25,7 @@ class LinkupSearchImageResult(BaseModel): An image result from a Linkup search. Attributes: - type: The type of the search result, in this case 'image' + type: The type of the search result, in this case "image". name: The name of the image result. url: The URL of the image result. """ @@ -43,7 +43,7 @@ class LinkupSearchResults(BaseModel): results: The results of the Linkup search. """ - results: List[Union[LinkupSearchTextResult, LinkupSearchImageResult]] + results: list[Union[LinkupSearchTextResult, LinkupSearchImageResult]] class LinkupSource(BaseModel): @@ -71,4 +71,4 @@ class LinkupSourcedAnswer(BaseModel): """ answer: str - sources: List[Union[LinkupSource, LinkupSearchTextResult, LinkupSearchImageResult]] + sources: list[Union[LinkupSource, LinkupSearchTextResult, LinkupSearchImageResult]] diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index a74b48d..4259b4b 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -1,5 +1,5 @@ import json -from typing import Any, List, Type, Union +from typing import Any, Union import pytest from httpx import Response @@ -27,7 +27,7 @@ class Company(BaseModel): name: str creation_date: str website_url: str - founders_names: List[str] + founders_names: list[str] def test_search_search_results(mocker: MockerFixture, client: LinkupClient) -> None: @@ -130,12 +130,8 @@ def test_search_sourced_answer(mocker: MockerFixture, client: LinkupClient) -> N def test_search_structured_search( mocker: MockerFixture, client: LinkupClient, - structured_output_schema: Union[Type[BaseModel], str], + structured_output_schema: Union[type[BaseModel], str], ) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "structured" - mocker.patch( "linkup.client.LinkupClient._request", return_value=Response( @@ -146,9 +142,9 @@ def test_search_structured_search( ) response: Any = client.search( - query=query, - depth=depth, - output_type=output_type, + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="structured", structured_output_schema=structured_output_schema, ) @@ -170,10 +166,6 @@ def test_search_structured_search( def test_search_authorization_error(mocker: MockerFixture, client: LinkupClient) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "searchResults" - mock_response = mocker.Mock() mock_response.status_code = 403 mock_response.json.return_value = { @@ -191,14 +183,14 @@ def test_search_authorization_error(mocker: MockerFixture, client: LinkupClient) ) with pytest.raises(LinkupAuthenticationError): - client.search(query=query, depth=depth, output_type=output_type) + client.search( + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="searchResults", + ) def test_search_authentication_error(mocker: MockerFixture, client: LinkupClient) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "searchResults" - mock_response = mocker.Mock() mock_response.status_code = 401 mock_response.json.return_value = { @@ -216,7 +208,11 @@ def test_search_authentication_error(mocker: MockerFixture, client: LinkupClient ) with pytest.raises(LinkupAuthenticationError): - client.search(query=query, depth=depth, output_type=output_type) + client.search( + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="searchResults", + ) def test_search_insufficient_credit_error(mocker: MockerFixture, client: LinkupClient) -> None: @@ -286,26 +282,6 @@ def test_search_structured_search_invalid_request( mocker: MockerFixture, client: LinkupClient, ) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "structured" - # Schema corresponding to the Company class, without "type": "object" - structured_output_schema = json.dumps( - { - "properties": { - "name": {"title": "Name", "type": "string"}, - "creation_date": {"title": "Creation Date", "type": "string"}, - "website_url": {"title": "Website Url", "type": "string"}, - "founders_names": { - "items": {"type": "string"}, - "title": "Founders Names", - "type": "array", - }, - }, - "required": ["name", "creation_date", "website_url", "founders_names"], - "title": "Company", - } - ) mock_response = mocker.Mock() mock_response.status_code = 400 mock_response.json.return_value = { @@ -326,10 +302,26 @@ def test_search_structured_search_invalid_request( with pytest.raises(LinkupInvalidRequestError): client.search( - query=query, - depth=depth, - output_type=output_type, - structured_output_schema=structured_output_schema, + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="structured", + # Schema corresponding to the Company class, without "type": "object" + structured_output_schema=json.dumps( + { + "properties": { + "name": {"title": "Name", "type": "string"}, + "creation_date": {"title": "Creation Date", "type": "string"}, + "website_url": {"title": "Website Url", "type": "string"}, + "founders_names": { + "items": {"type": "string"}, + "title": "Founders Names", + "type": "array", + }, + }, + "required": ["name", "creation_date", "website_url", "founders_names"], + "title": "Company", + } + ), ) @@ -354,10 +346,6 @@ def test_search_no_result_error(mocker: MockerFixture, client: LinkupClient) -> def test_search_unknown_error(mocker: MockerFixture, client: LinkupClient) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "searchResults" - mock_response = mocker.Mock() mock_response.status_code = 500 mock_response.json.return_value = { @@ -375,7 +363,11 @@ def test_search_unknown_error(mocker: MockerFixture, client: LinkupClient) -> No ) with pytest.raises(LinkupUnknownError): - client.search(query=query, depth=depth, output_type=output_type) + client.search( + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="searchResults", + ) @pytest.mark.asyncio @@ -485,12 +477,8 @@ async def test_async_search_sourced_answer(mocker: MockerFixture, client: Linkup async def test_async_search_structured_search( mocker: MockerFixture, client: LinkupClient, - structured_output_schema: Union[Type[BaseModel], str], + structured_output_schema: Union[type[BaseModel], str], ) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "structured" - mocker.patch( "linkup.client.LinkupClient._async_request", return_value=Response( @@ -501,9 +489,9 @@ async def test_async_search_structured_search( ) response: Any = await client.async_search( - query=query, - depth=depth, - output_type=output_type, + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="structured", structured_output_schema=structured_output_schema, ) @@ -528,10 +516,6 @@ async def test_async_search_structured_search( async def test_async_search_authorization_error( mocker: MockerFixture, client: LinkupClient ) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "searchResults" - mock_response = mocker.Mock() mock_response.status_code = 403 mock_response.json.return_value = { @@ -549,17 +533,17 @@ async def test_async_search_authorization_error( ) with pytest.raises(LinkupAuthenticationError): - await client.async_search(query=query, depth=depth, output_type=output_type) + await client.async_search( + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="searchResults", + ) @pytest.mark.asyncio async def test_async_search_authentication_error( mocker: MockerFixture, client: LinkupClient ) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "searchResults" - mock_response = mocker.Mock() mock_response.status_code = 401 mock_response.json.return_value = { @@ -577,7 +561,11 @@ async def test_async_search_authentication_error( ) with pytest.raises(LinkupAuthenticationError): - await client.async_search(query=query, depth=depth, output_type=output_type) + await client.async_search( + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="searchResults", + ) @pytest.mark.asyncio @@ -654,27 +642,6 @@ async def test_async_search_structured_search_invalid_request( mocker: MockerFixture, client: LinkupClient, ) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "structured" - # Schema corresponding to the Company class, without "type": "object" - structured_output_schema = json.dumps( - { - "properties": { - "name": {"title": "Name", "type": "string"}, - "creation_date": {"title": "Creation Date", "type": "string"}, - "website_url": {"title": "Website Url", "type": "string"}, - "founders_names": { - "items": {"type": "string"}, - "title": "Founders Names", - "type": "array", - }, - }, - "required": ["name", "creation_date", "website_url", "founders_names"], - "title": "Company", - } - ) - mock_response = mocker.Mock() mock_response.status_code = 400 mock_response.json.return_value = { @@ -698,10 +665,26 @@ async def test_async_search_structured_search_invalid_request( with pytest.raises(LinkupInvalidRequestError): await client.async_search( - query=query, - depth=depth, - output_type=output_type, - structured_output_schema=structured_output_schema, + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="structured", + # Schema corresponding to the Company class, without "type": "object" + structured_output_schema=json.dumps( + { + "properties": { + "name": {"title": "Name", "type": "string"}, + "creation_date": {"title": "Creation Date", "type": "string"}, + "website_url": {"title": "Website Url", "type": "string"}, + "founders_names": { + "items": {"type": "string"}, + "title": "Founders Names", + "type": "array", + }, + }, + "required": ["name", "creation_date", "website_url", "founders_names"], + "title": "Company", + } + ), ) @@ -728,10 +711,6 @@ async def test_async_search_no_result_error(mocker: MockerFixture, client: Linku @pytest.mark.asyncio async def test_async_search_unknown_error(mocker: MockerFixture, client: LinkupClient) -> None: - query = "What is Linkup, the new French AI company?" - depth = "standard" - output_type = "searchResults" - mock_response = mocker.Mock() mock_response.status_code = 500 mock_response.json.return_value = { @@ -749,4 +728,8 @@ async def test_async_search_unknown_error(mocker: MockerFixture, client: LinkupC ) with pytest.raises(LinkupUnknownError): - await client.async_search(query=query, depth=depth, output_type=output_type) + await client.async_search( + query="What is Linkup, the new French AI company?", + depth="standard", + output_type="searchResults", + ) From d8118333c9ede329f790618068afa7b6b18990e1 Mon Sep 17 00:00:00 2001 From: cjumel Date: Tue, 16 Sep 2025 10:27:15 +0200 Subject: [PATCH 4/7] fix: types and documentation improvements --- src/linkup/client.py | 6 ++-- src/linkup/types.py | 12 ++++---- tests/unit/client_test.py | 62 ++++++++++++++++----------------------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/src/linkup/client.py b/src/linkup/client.py index 3fe9195..a12b67d 100644 --- a/src/linkup/client.py +++ b/src/linkup/client.py @@ -72,8 +72,7 @@ def search( structured_output_schema: If output_type is "structured", specify the schema of the output. Supported formats are a pydantic.BaseModel or a string representing a valid object JSON schema. - include_images: If output_type is "searchResults", specifies if the response can include - images. Default to False. + include_images: Specify if the search should try and include images. 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. from_date: The date from which the search results should be considered. If None, the @@ -150,8 +149,7 @@ async def async_search( structured_output_schema: If output_type is "structured", specify the schema of the output. Supported formats are a pydantic.BaseModel or a string representing a valid object JSON schema. - include_images: If output_type is "searchResults", specifies if the response can include - images. Default to False + include_images: Specify if the search should try and include images. 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. from_date: The date from which the search results should be considered. If None, the diff --git a/src/linkup/types.py b/src/linkup/types.py index 3d6128c..c975bea 100644 --- a/src/linkup/types.py +++ b/src/linkup/types.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Literal, Union from pydantic import BaseModel @@ -14,7 +14,7 @@ class LinkupSearchTextResult(BaseModel): content: The text of the search result. """ - type: str + type: Literal["text"] name: str url: str content: str @@ -30,7 +30,7 @@ class LinkupSearchImageResult(BaseModel): url: The URL of the image result. """ - type: str + type: Literal["image"] name: str url: str @@ -53,12 +53,12 @@ class LinkupSource(BaseModel): Attributes: name: The name of the source. url: The URL of the source. - snippet: The text excerpt supporting the Linkup answer. + snippet: The text excerpt supporting the Linkup answer. Can be empty for image sources. """ name: str url: str - snippet: str + snippet: str = "" class LinkupSourcedAnswer(BaseModel): @@ -71,4 +71,4 @@ class LinkupSourcedAnswer(BaseModel): """ answer: str - sources: list[Union[LinkupSource, LinkupSearchTextResult, LinkupSearchImageResult]] + sources: list[LinkupSource] diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 4259b4b..09bf205 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -76,19 +76,17 @@ def test_search_sourced_answer(mocker: MockerFixture, client: LinkupClient) -> N "sources": [ { "name": "foo", - "url": "https://foo.bar/baz", - "snippet": "foo bar baz qux" + "url": "https://foo.com", + "snippet": "lorem ipsum dolor sit amet" }, { - "type": "text", "name": "bar", - "url": "https://foo.bar/baz", - "content": "foo bar baz qux" + "url": "https://bar.com", + "snippet": "consectetur adipiscing elit" }, { - "type": "image", "name": "baz", - "url": "https://foo.bar/baz" + "url": "https://baz.com" } ] } @@ -108,19 +106,16 @@ def test_search_sourced_answer(mocker: MockerFixture, client: LinkupClient) -> N assert isinstance(response.sources[0], LinkupSource) assert response.answer == "foo bar baz" assert response.sources[0].name == "foo" - assert response.sources[0].url == "https://foo.bar/baz" - assert response.sources[0].snippet == "foo bar baz qux" - assert isinstance(response.sources[1], LinkupSearchTextResult) - assert response.answer == "foo bar baz" - assert response.sources[1].type == "text" + assert response.sources[0].url == "https://foo.com" + assert response.sources[0].snippet == "lorem ipsum dolor sit amet" + assert isinstance(response.sources[1], LinkupSource) assert response.sources[1].name == "bar" - assert response.sources[1].url == "https://foo.bar/baz" - assert response.sources[1].content == "foo bar baz qux" - assert isinstance(response.sources[2], LinkupSearchImageResult) - assert response.answer == "foo bar baz" - assert response.sources[2].type == "image" + assert response.sources[1].url == "https://bar.com" + assert response.sources[1].snippet == "consectetur adipiscing elit" + assert isinstance(response.sources[2], LinkupSource) assert response.sources[2].name == "baz" - assert response.sources[2].url == "https://foo.bar/baz" + assert response.sources[2].url == "https://baz.com" + assert response.sources[2].snippet == "" @pytest.mark.parametrize( @@ -420,19 +415,17 @@ async def test_async_search_sourced_answer(mocker: MockerFixture, client: Linkup "sources": [ { "name": "foo", - "url": "https://foo.bar/baz", - "snippet": "foo bar baz qux" + "url": "https://foo.com", + "snippet": "lorem ipsum dolor sit amet" }, { - "type": "text", "name": "bar", - "url": "https://foo.bar/baz", - "content": "foo bar baz qux" + "url": "https://bar.com", + "snippet": "consectetur adipiscing elit" }, { - "type": "image", "name": "baz", - "url": "https://foo.bar/baz" + "url": "https://baz.com" } ] } @@ -454,19 +447,16 @@ async def test_async_search_sourced_answer(mocker: MockerFixture, client: Linkup assert isinstance(response.sources[0], LinkupSource) assert response.answer == "foo bar baz" assert response.sources[0].name == "foo" - assert response.sources[0].url == "https://foo.bar/baz" - assert response.sources[0].snippet == "foo bar baz qux" - assert isinstance(response.sources[1], LinkupSearchTextResult) - assert response.answer == "foo bar baz" - assert response.sources[1].type == "text" + assert response.sources[0].url == "https://foo.com" + assert response.sources[0].snippet == "lorem ipsum dolor sit amet" + assert isinstance(response.sources[1], LinkupSource) assert response.sources[1].name == "bar" - assert response.sources[1].url == "https://foo.bar/baz" - assert response.sources[1].content == "foo bar baz qux" - assert isinstance(response.sources[2], LinkupSearchImageResult) - assert response.answer == "foo bar baz" - assert response.sources[2].type == "image" + assert response.sources[1].url == "https://bar.com" + assert response.sources[1].snippet == "consectetur adipiscing elit" + assert isinstance(response.sources[2], LinkupSource) assert response.sources[2].name == "baz" - assert response.sources[2].url == "https://foo.bar/baz" + assert response.sources[2].url == "https://baz.com" + assert response.sources[2].snippet == "" @pytest.mark.asyncio From 4b720b265ebfca744220618afa4e0a5871df14ec Mon Sep 17 00:00:00 2001 From: cjumel Date: Tue, 16 Sep 2025 10:59:06 +0200 Subject: [PATCH 5/7] chore: update PR template --- .github/pull_request_template.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d1b46f0..f827401 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,12 +1,12 @@ ## Description - + ## Checklist - + -- [ ] I have run the tests locally with `make install-dev` and `make test` +- [ ] 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 -- [ ] I have updated the package version in `linkup/_version.py` and `pyproject.toml` if I plan on - creating a new release From a46f312d97f9c1e4b53393ad9a8f8e4ce8cf00b1 Mon Sep 17 00:00:00 2001 From: cjumel Date: Tue, 16 Sep 2025 11:32:10 +0200 Subject: [PATCH 6/7] chore: use rich.print in examples --- examples/1_direct_search_results.py | 2 + examples/2_sourced_answer_search.py | 2 + examples/3_structured_search.py | 1 + examples/4_asynchronous_search.py | 2 + pyproject.toml | 1 + uv.lock | 59 +++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+) diff --git a/examples/1_direct_search_results.py b/examples/1_direct_search_results.py index b1d8748..34769ba 100644 --- a/examples/1_direct_search_results.py +++ b/examples/1_direct_search_results.py @@ -3,6 +3,8 @@ for instance in a RAG system, with the output_type parameter set to "searchResults". """ +from rich import print + from linkup import LinkupClient client = LinkupClient() diff --git a/examples/2_sourced_answer_search.py b/examples/2_sourced_answer_search.py index 813c8e2..0f565f4 100644 --- a/examples/2_sourced_answer_search.py +++ b/examples/2_sourced_answer_search.py @@ -4,6 +4,8 @@ along with the sources supporting it. """ +from rich import print + from linkup import LinkupClient client = LinkupClient() diff --git a/examples/3_structured_search.py b/examples/3_structured_search.py index da12a8b..00e7a1b 100644 --- a/examples/3_structured_search.py +++ b/examples/3_structured_search.py @@ -5,6 +5,7 @@ """ from pydantic import BaseModel, Field +from rich import print from linkup import LinkupClient diff --git a/examples/4_asynchronous_search.py b/examples/4_asynchronous_search.py index 99be9b2..6ea3d48 100644 --- a/examples/4_asynchronous_search.py +++ b/examples/4_asynchronous_search.py @@ -7,6 +7,8 @@ import asyncio import time +from rich import print + from linkup import LinkupClient client = LinkupClient() diff --git a/pyproject.toml b/pyproject.toml index 1ceb9be..5e627d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dev = [ "pytest-cov>=6.2.1", "pytest-mock>=3.14.1", "pytest>=8.4.1", + "rich>=14.1.0", ] [tool.mypy] diff --git a/uv.lock b/uv.lock index 95ae93b..5665cc5 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] [[package]] name = "annotated-types" @@ -257,6 +261,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-mock" }, + { name = "rich" }, ] [package.metadata] @@ -275,6 +280,46 @@ dev = [ { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, { name = "pytest-mock", specifier = ">=3.14.1" }, + { name = "rich", specifier = ">=14.1.0" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -636,6 +681,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" From fa4f3ee7a3e4b11873c6d2a901c168cec30bd59a Mon Sep 17 00:00:00 2001 From: cjumel Date: Tue, 16 Sep 2025 16:19:44 +0200 Subject: [PATCH 7/7] fix: small docstring wording --- src/linkup/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linkup/client.py b/src/linkup/client.py index a12b67d..cf0b7f6 100644 --- a/src/linkup/client.py +++ b/src/linkup/client.py @@ -72,7 +72,7 @@ def search( structured_output_schema: If output_type is "structured", specify the schema of the output. Supported formats are a pydantic.BaseModel or a string representing a valid object JSON schema. - include_images: Specify if the search should try and include images. + include_images: Indicate whether images should be included during the search. 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. from_date: The date from which the search results should be considered. If None, the @@ -149,7 +149,7 @@ async def async_search( structured_output_schema: If output_type is "structured", specify the schema of the output. Supported formats are a pydantic.BaseModel or a string representing a valid object JSON schema. - include_images: Specify if the search should try and include images. + include_images: Indicate whether images should be included during the search. 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. from_date: The date from which the search results should be considered. If None, the