brave-api-client 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brave_api_client-0.0.4/.github/workflows/ci.yml +50 -0
- brave_api_client-0.0.4/.github/workflows/publish.yml +19 -0
- brave_api_client-0.0.4/.gitignore +174 -0
- brave_api_client-0.0.4/.pre-commit-config.yaml +36 -0
- brave_api_client-0.0.4/LICENSE +21 -0
- brave_api_client-0.0.4/PKG-INFO +16 -0
- brave_api_client-0.0.4/README.md +1 -0
- brave_api_client-0.0.4/brave_api/__init__.py +0 -0
- brave_api_client-0.0.4/brave_api/client.py +98 -0
- brave_api_client-0.0.4/brave_api/constants.py +3 -0
- brave_api_client-0.0.4/brave_api/web_search/__init__.py +0 -0
- brave_api_client-0.0.4/brave_api/web_search/models.py +692 -0
- brave_api_client-0.0.4/justfile +33 -0
- brave_api_client-0.0.4/pyproject.toml +46 -0
- brave_api_client-0.0.4/tests/__init__.py +0 -0
- brave_api_client-0.0.4/tests/web_search/__init__.py +0 -0
- brave_api_client-0.0.4/tests/web_search/test_models.py +9 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Build and test python application
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "main" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "main" ]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.head_ref || github.run_id }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
|
|
18
|
+
lint:
|
|
19
|
+
name: Lint
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: extractions/setup-just@v3
|
|
24
|
+
- uses: astral-sh/setup-uv@v6
|
|
25
|
+
with:
|
|
26
|
+
cache-dependency-glob: "**/pyproject.toml"
|
|
27
|
+
- run: uv python install 3.10
|
|
28
|
+
- run: just install
|
|
29
|
+
- run: just lint
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
name: Test
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
python-version:
|
|
38
|
+
- "3.10"
|
|
39
|
+
- "3.11"
|
|
40
|
+
- "3.12"
|
|
41
|
+
- "3.13"
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
- uses: extractions/setup-just@v3
|
|
45
|
+
- uses: astral-sh/setup-uv@v6
|
|
46
|
+
with:
|
|
47
|
+
cache-dependency-glob: "**/pyproject.toml"
|
|
48
|
+
- run: uv python install ${{ matrix.python-version }}
|
|
49
|
+
- run: just install
|
|
50
|
+
- run: just test . --cov=. --cov-report xml
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types:
|
|
6
|
+
- published
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: extractions/setup-just@v2
|
|
14
|
+
- uses: astral-sh/setup-uv@v6
|
|
15
|
+
with:
|
|
16
|
+
cache-dependency-glob: "**/pyproject.toml"
|
|
17
|
+
- run: just publish
|
|
18
|
+
env:
|
|
19
|
+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
.idea/
|
|
169
|
+
|
|
170
|
+
# Ruff stuff:
|
|
171
|
+
.ruff_cache/
|
|
172
|
+
|
|
173
|
+
# PyPI configuration file
|
|
174
|
+
.pypirc
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
repos:
|
|
3
|
+
|
|
4
|
+
- repo: https://github.com/codespell-project/codespell
|
|
5
|
+
rev: v2.3.0
|
|
6
|
+
hooks:
|
|
7
|
+
- id: codespell
|
|
8
|
+
additional_dependencies:
|
|
9
|
+
- tomli
|
|
10
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
11
|
+
rev: "v0.11.11"
|
|
12
|
+
hooks:
|
|
13
|
+
- id: ruff
|
|
14
|
+
name: ruff
|
|
15
|
+
files: "^brave_api|^tests|^examples|^docs"
|
|
16
|
+
args: [ --fix ]
|
|
17
|
+
- id: ruff-format
|
|
18
|
+
files: "^brave_api|^tests|^examples|^docs"
|
|
19
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
20
|
+
rev: v5.0.0
|
|
21
|
+
hooks:
|
|
22
|
+
- id: end-of-file-fixer
|
|
23
|
+
- id: mixed-line-ending
|
|
24
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
25
|
+
rev: 'v1.15.0'
|
|
26
|
+
hooks:
|
|
27
|
+
- id: mypy
|
|
28
|
+
additional_dependencies: [pydantic]
|
|
29
|
+
- repo: local
|
|
30
|
+
hooks:
|
|
31
|
+
- id: tests
|
|
32
|
+
name: tests
|
|
33
|
+
entry: uv run pytest tests
|
|
34
|
+
language: system
|
|
35
|
+
types: [ python ]
|
|
36
|
+
pass_filenames: false
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brave-api-client
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Brave API client for Python
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Typing :: Typed
|
|
11
|
+
Requires-Python: <4,>=3.10
|
|
12
|
+
Requires-Dist: httpx>=0.28.1
|
|
13
|
+
Requires-Dist: pydantic>=2.11.5
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# brave-api
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# brave-api
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import os
|
|
3
|
+
import typing
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from brave_api.constants import BRAVE_API_KEY_ENV_VAR
|
|
8
|
+
from brave_api.web_search.models import WebSearchApiResponse, WebSearchQueryParams
|
|
9
|
+
from contextlib import AbstractAsyncContextManager
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_api_key_from_env() -> str | None:
|
|
14
|
+
return os.getenv(BRAVE_API_KEY_ENV_VAR)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _BraveAPIClientBase(abc.ABC):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
base_url: str | None = None,
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
proxy: str | None = None,
|
|
23
|
+
):
|
|
24
|
+
self.base_url = base_url or "https://api.search.brave.com/res/v1"
|
|
25
|
+
self._provided_api_key = api_key
|
|
26
|
+
self._proxy = proxy
|
|
27
|
+
|
|
28
|
+
def _build_search_url(self) -> str:
|
|
29
|
+
return f"{self.base_url}/web/search"
|
|
30
|
+
|
|
31
|
+
def _build_search_params(
|
|
32
|
+
self, query_params: WebSearchQueryParams
|
|
33
|
+
) -> tuple[str, dict[str, str], dict[str, typing.Any]]:
|
|
34
|
+
url = self._build_search_url()
|
|
35
|
+
api_key = self._provided_api_key or _get_api_key_from_env()
|
|
36
|
+
if not api_key:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
"API key is required. Set it via environment variable BRAVE_API_KEY or pass it to the client."
|
|
39
|
+
)
|
|
40
|
+
headers = {
|
|
41
|
+
"X-Subscription-Token": api_key,
|
|
42
|
+
}
|
|
43
|
+
return url, headers, query_params.model_dump(exclude_unset=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AsyncBraveAPIClient(
|
|
47
|
+
_BraveAPIClientBase, AbstractAsyncContextManager["AsyncBraveAPIClient"]
|
|
48
|
+
):
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
52
|
+
*args: typing.Any,
|
|
53
|
+
**kwargs: typing.Any,
|
|
54
|
+
):
|
|
55
|
+
super().__init__(*args, **kwargs)
|
|
56
|
+
self._client = client if client else httpx.AsyncClient()
|
|
57
|
+
self._transport: Optional[httpx.AsyncClient] = None
|
|
58
|
+
|
|
59
|
+
async def __aenter__(self) -> "AsyncBraveAPIClient":
|
|
60
|
+
self._transport = await self._client.__aenter__()
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
async def __aexit__(
|
|
64
|
+
self,
|
|
65
|
+
exc_type: type[BaseException] | None,
|
|
66
|
+
exc_value: BaseException | None,
|
|
67
|
+
traceback: TracebackType | None,
|
|
68
|
+
/,
|
|
69
|
+
) -> None:
|
|
70
|
+
self._transport = None
|
|
71
|
+
await self._client.aclose()
|
|
72
|
+
|
|
73
|
+
def _verify_transport(self) -> None:
|
|
74
|
+
if self._transport is None:
|
|
75
|
+
raise RuntimeError("Use async with `AsyncBraveAPIClient()`")
|
|
76
|
+
|
|
77
|
+
async def search(self, query: WebSearchQueryParams) -> WebSearchApiResponse:
|
|
78
|
+
self._verify_transport()
|
|
79
|
+
url, headers, query_params = self._build_search_params(query)
|
|
80
|
+
response = await self._transport.get( # type: ignore[union-attr]
|
|
81
|
+
url, headers=headers, params=query_params
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
return WebSearchApiResponse.model_validate(response.json())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class BraveAPIClient(_BraveAPIClientBase):
|
|
89
|
+
"""
|
|
90
|
+
A client for interacting with the Brave API.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def search(self, query: WebSearchQueryParams) -> WebSearchApiResponse:
|
|
94
|
+
url, headers, query_params = self._build_search_params(query)
|
|
95
|
+
response = httpx.get(url=url, headers=headers, params=query_params)
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
|
|
98
|
+
return WebSearchApiResponse.model_validate(response.json())
|
|
File without changes
|
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
from typing import List, Optional, Union, Literal, Any
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
# Base and utility models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Thumbnail(BaseModel):
|
|
8
|
+
src: str
|
|
9
|
+
original: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Profile(BaseModel):
|
|
13
|
+
name: str
|
|
14
|
+
long_name: str
|
|
15
|
+
url: Optional[str] = None
|
|
16
|
+
img: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Rating(BaseModel):
|
|
20
|
+
ratingValue: float
|
|
21
|
+
bestRating: float
|
|
22
|
+
reviewCount: Optional[int] = None
|
|
23
|
+
profile: Optional[Profile] = None
|
|
24
|
+
is_tripadvisor: bool = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Thing(BaseModel):
|
|
28
|
+
type: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Person(BaseModel):
|
|
32
|
+
type: str
|
|
33
|
+
email: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MetaUrl(BaseModel):
|
|
37
|
+
scheme: str
|
|
38
|
+
netloc: str
|
|
39
|
+
hostname: Optional[str] = None
|
|
40
|
+
favicon: str
|
|
41
|
+
path: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Contact(BaseModel):
|
|
45
|
+
email: Optional[str] = None
|
|
46
|
+
telephone: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ContactPoint(Thing):
|
|
50
|
+
type: Literal["contact_point"] = "contact_point"
|
|
51
|
+
telephone: Optional[str] = None
|
|
52
|
+
email: Optional[str] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DataProvider(BaseModel):
|
|
56
|
+
type: Literal["external"] = "external"
|
|
57
|
+
name: str
|
|
58
|
+
url: str
|
|
59
|
+
long_name: Optional[str] = None
|
|
60
|
+
img: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Unit(BaseModel):
|
|
64
|
+
value: float
|
|
65
|
+
units: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Answer(BaseModel):
|
|
69
|
+
text: str
|
|
70
|
+
author: Optional[str] = None
|
|
71
|
+
upvoteCount: Optional[int] = None
|
|
72
|
+
downvoteCount: Optional[int] = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class QAPage(BaseModel):
|
|
76
|
+
question: str
|
|
77
|
+
answer: Answer
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class QA(BaseModel):
|
|
81
|
+
question: str
|
|
82
|
+
answer: str
|
|
83
|
+
title: str
|
|
84
|
+
url: str
|
|
85
|
+
meta_url: Optional[MetaUrl] = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FAQ(BaseModel):
|
|
89
|
+
type: Literal["faq"] = "faq"
|
|
90
|
+
results: List[QA]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ForumData(BaseModel):
|
|
94
|
+
forum_name: str
|
|
95
|
+
num_answers: Optional[int] = None
|
|
96
|
+
score: Optional[str] = None
|
|
97
|
+
title: Optional[str] = None
|
|
98
|
+
question: Optional[str] = None
|
|
99
|
+
top_comment: Optional[str] = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DiscussionResult(BaseModel):
|
|
103
|
+
type: Literal["discussion"] = "discussion"
|
|
104
|
+
data: Optional[ForumData] = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Discussions(BaseModel):
|
|
108
|
+
type: Literal["search"] = "search"
|
|
109
|
+
results: List[DiscussionResult]
|
|
110
|
+
mutated_by_goggles: bool = False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SearchResult(BaseModel):
|
|
114
|
+
type: Literal["search_result"] = "search_result"
|
|
115
|
+
subtype: str = "generic"
|
|
116
|
+
is_live: bool = False
|
|
117
|
+
deep_results: Optional["DeepResult"] = None
|
|
118
|
+
schemas: Optional[List[List[Any]]] = None
|
|
119
|
+
meta_url: Optional[MetaUrl] = None
|
|
120
|
+
thumbnail: Optional[Thumbnail] = None
|
|
121
|
+
age: Optional[str] = None
|
|
122
|
+
language: str
|
|
123
|
+
location: Optional["LocationResult"] = None
|
|
124
|
+
video: Optional["VideoData"] = None
|
|
125
|
+
movie: Optional["MovieData"] = None
|
|
126
|
+
faq: Optional[FAQ] = None
|
|
127
|
+
qa: Optional[QAPage] = None
|
|
128
|
+
book: Optional["Book"] = None
|
|
129
|
+
rating: Optional[Rating] = None
|
|
130
|
+
article: Optional["Article"] = None
|
|
131
|
+
product: Optional[Union["Product", "Review"]] = None
|
|
132
|
+
product_cluster: Optional[List[Union["Product", "Review"]]] = None
|
|
133
|
+
cluster_type: Optional[str] = None
|
|
134
|
+
cluster: Optional[List["Result"]] = None
|
|
135
|
+
creative_work: Optional["CreativeWork"] = None
|
|
136
|
+
music_recording: Optional["MusicRecording"] = None
|
|
137
|
+
review: Optional["Review"] = None
|
|
138
|
+
software: Optional["Software"] = None
|
|
139
|
+
recipe: Optional["Recipe"] = None
|
|
140
|
+
organization: Optional["Organization"] = None
|
|
141
|
+
content_type: Optional[str] = None
|
|
142
|
+
extra_snippets: Optional[List[str]] = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Result(BaseModel):
|
|
146
|
+
title: str
|
|
147
|
+
url: str
|
|
148
|
+
is_source_local: bool = True
|
|
149
|
+
is_source_both: bool = True
|
|
150
|
+
description: Optional[str] = None
|
|
151
|
+
page_age: Optional[str] = None
|
|
152
|
+
page_fetched: Optional[str] = None
|
|
153
|
+
profile: Optional[Profile] = None
|
|
154
|
+
language: Optional[str] = None
|
|
155
|
+
family_friendly: bool = True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class LocationWebResult(Result):
|
|
159
|
+
meta_url: MetaUrl
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class LocationResult(Result):
|
|
163
|
+
type: Literal["location_result"] = "location_result"
|
|
164
|
+
id: Optional[str] = None
|
|
165
|
+
provider_url: str
|
|
166
|
+
coordinates: Optional[List[float]] = None
|
|
167
|
+
zoom_level: int
|
|
168
|
+
thumbnail: Optional[Thumbnail] = None
|
|
169
|
+
postal_address: Optional["PostalAddress"] = None
|
|
170
|
+
opening_hours: Optional["OpeningHours"] = None
|
|
171
|
+
contact: Optional[Contact] = None
|
|
172
|
+
price_range: Optional[str] = None
|
|
173
|
+
rating: Optional[Rating] = None
|
|
174
|
+
distance: Optional[Unit] = None
|
|
175
|
+
profiles: Optional[List[DataProvider]] = None
|
|
176
|
+
reviews: Optional["Reviews"] = None
|
|
177
|
+
pictures: Optional["PictureResults"] = None
|
|
178
|
+
action: Optional["Action"] = None
|
|
179
|
+
serves_cuisine: Optional[List[str]] = None
|
|
180
|
+
categories: Optional[List[str]] = None
|
|
181
|
+
icon_category: Optional[str] = None
|
|
182
|
+
results: Optional[LocationWebResult] = None
|
|
183
|
+
timezone: Optional[str] = None
|
|
184
|
+
timezone_offset: Optional[str] = None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Locations(BaseModel):
|
|
188
|
+
type: Literal["locations"] = "locations"
|
|
189
|
+
results: List[LocationResult]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Summarizer(BaseModel):
|
|
193
|
+
type: Literal["summarizer"] = "summarizer"
|
|
194
|
+
key: str
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class RichCallbackHint(BaseModel):
|
|
198
|
+
vertical: str
|
|
199
|
+
callback_key: str
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class RichCallbackInfo(BaseModel):
|
|
203
|
+
type: Literal["rich"] = "rich"
|
|
204
|
+
hint: Optional[RichCallbackHint] = None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ResultReference(BaseModel):
|
|
208
|
+
type: str
|
|
209
|
+
index: Optional[int] = None
|
|
210
|
+
all: bool
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class MixedResponse(BaseModel):
|
|
214
|
+
type: Literal["mixed"] = "mixed"
|
|
215
|
+
main: Optional[List[ResultReference]] = None
|
|
216
|
+
top: Optional[List[ResultReference]] = None
|
|
217
|
+
side: Optional[List[ResultReference]] = None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class LocalPoiSearchApiResponse(BaseModel):
|
|
221
|
+
type: Literal["local_pois"] = "local_pois"
|
|
222
|
+
results: Optional[List[LocationResult]] = None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class LocalDescriptionsSearchApiResponse(BaseModel):
|
|
226
|
+
type: Literal["local_descriptions"] = "local_descriptions"
|
|
227
|
+
results: Optional[List["LocationDescription"]] = None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class LocationDescription(BaseModel):
|
|
231
|
+
type: Literal["local_description"] = "local_description"
|
|
232
|
+
id: str
|
|
233
|
+
description: Optional[str] = None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class WebSearchApiResponse(BaseModel):
|
|
237
|
+
type: Literal["search"]
|
|
238
|
+
discussions: Optional[Discussions] = None
|
|
239
|
+
faq: Optional[FAQ] = None
|
|
240
|
+
infobox: Optional["GraphInfobox"] = None
|
|
241
|
+
locations: Optional[Locations] = None
|
|
242
|
+
mixed: Optional[MixedResponse] = None
|
|
243
|
+
news: Optional["News"] = None
|
|
244
|
+
query: Optional["Query"] = None
|
|
245
|
+
videos: Optional["Videos"] = None
|
|
246
|
+
web: Optional["Search"] = None
|
|
247
|
+
summarizer: Optional[Summarizer] = None
|
|
248
|
+
rich: Optional[RichCallbackInfo] = None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class Search(BaseModel):
|
|
252
|
+
type: Literal["search"] = "search"
|
|
253
|
+
results: List[SearchResult]
|
|
254
|
+
family_friendly: bool
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class Query(BaseModel):
|
|
258
|
+
original: str
|
|
259
|
+
show_strict_warning: Optional[bool] = None
|
|
260
|
+
altered: Optional[str] = None
|
|
261
|
+
safesearch: Optional[bool] = None
|
|
262
|
+
is_navigational: Optional[bool] = None
|
|
263
|
+
is_geolocal: Optional[bool] = None
|
|
264
|
+
local_decision: Optional[str] = None
|
|
265
|
+
local_locations_idx: Optional[int] = None
|
|
266
|
+
is_trending: Optional[bool] = None
|
|
267
|
+
is_news_breaking: Optional[bool] = None
|
|
268
|
+
ask_for_location: Optional[bool] = None
|
|
269
|
+
language: Optional["Language"] = None
|
|
270
|
+
spellcheck_off: Optional[bool] = None
|
|
271
|
+
country: Optional[str] = None
|
|
272
|
+
bad_results: Optional[bool] = None
|
|
273
|
+
should_fallback: Optional[bool] = None
|
|
274
|
+
lat: Optional[str] = None
|
|
275
|
+
long: Optional[str] = None
|
|
276
|
+
postal_code: Optional[str] = None
|
|
277
|
+
city: Optional[str] = None
|
|
278
|
+
state: Optional[str] = None
|
|
279
|
+
header_country: Optional[str] = None
|
|
280
|
+
more_results_available: Optional[bool] = None
|
|
281
|
+
custom_location_label: Optional[str] = None
|
|
282
|
+
reddit_cluster: Optional[str] = None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class Language(BaseModel):
|
|
286
|
+
main: str
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class NewsResult(Result):
|
|
290
|
+
meta_url: Optional[MetaUrl] = None
|
|
291
|
+
source: Optional[str] = None
|
|
292
|
+
breaking: bool
|
|
293
|
+
is_live: bool
|
|
294
|
+
thumbnail: Optional[Thumbnail] = None
|
|
295
|
+
age: Optional[str] = None
|
|
296
|
+
extra_snippets: Optional[List[str]] = None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class News(BaseModel):
|
|
300
|
+
type: Literal["news"] = "news"
|
|
301
|
+
results: List[NewsResult]
|
|
302
|
+
mutated_by_goggles: bool = False
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class VideoData(BaseModel):
|
|
306
|
+
duration: Optional[str] = None
|
|
307
|
+
views: Optional[str] = None
|
|
308
|
+
creator: Optional[str] = None
|
|
309
|
+
publisher: Optional[str] = None
|
|
310
|
+
thumbnail: Optional[Thumbnail] = None
|
|
311
|
+
tags: Optional[List[str]] = None
|
|
312
|
+
author: Optional[Profile] = None
|
|
313
|
+
requires_subscription: Optional[bool] = None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class VideoResult(Result):
|
|
317
|
+
type: Literal["video_result"] = "video_result"
|
|
318
|
+
video: VideoData
|
|
319
|
+
meta_url: Optional[MetaUrl] = None
|
|
320
|
+
thumbnail: Optional[Thumbnail] = None
|
|
321
|
+
age: Optional[str] = None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class Videos(BaseModel):
|
|
325
|
+
type: Literal["videos"] = "videos"
|
|
326
|
+
results: List[VideoResult]
|
|
327
|
+
mutated_by_goggles: Optional[bool] = False
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class DeepResult(BaseModel):
|
|
331
|
+
news: Optional[List[NewsResult]] = None
|
|
332
|
+
buttons: Optional[List["ButtonResult"]] = None
|
|
333
|
+
videos: Optional[List[VideoResult]] = None
|
|
334
|
+
images: Optional[List["Image"]] = None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class ButtonResult(BaseModel):
|
|
338
|
+
type: Literal["button_result"] = "button_result"
|
|
339
|
+
title: str
|
|
340
|
+
url: str
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ImageProperties(BaseModel):
|
|
344
|
+
url: str
|
|
345
|
+
resized: str
|
|
346
|
+
placeholder: str
|
|
347
|
+
height: Optional[int] = None
|
|
348
|
+
width: Optional[int] = None
|
|
349
|
+
format: Optional[str] = None
|
|
350
|
+
content_size: Optional[str] = None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class Image(BaseModel):
|
|
354
|
+
thumbnail: Thumbnail
|
|
355
|
+
url: Optional[str] = None
|
|
356
|
+
properties: Optional[ImageProperties] = None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class Review(BaseModel):
|
|
360
|
+
type: Literal["review"] = "review"
|
|
361
|
+
name: str
|
|
362
|
+
thumbnail: Thumbnail
|
|
363
|
+
description: str
|
|
364
|
+
rating: Rating
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class Product(BaseModel):
|
|
368
|
+
type: Literal["product"] = "product"
|
|
369
|
+
name: str
|
|
370
|
+
category: Optional[str] = None
|
|
371
|
+
price: str
|
|
372
|
+
thumbnail: Thumbnail
|
|
373
|
+
description: Optional[str] = None
|
|
374
|
+
offers: Optional[List["Offer"]] = None
|
|
375
|
+
rating: Optional[Rating] = None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class Offer(BaseModel):
|
|
379
|
+
url: str
|
|
380
|
+
priceCurrency: str
|
|
381
|
+
price: str
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class Book(BaseModel):
|
|
385
|
+
title: str
|
|
386
|
+
author: List[Person]
|
|
387
|
+
date: Optional[str] = None
|
|
388
|
+
price: Optional["Price"] = None
|
|
389
|
+
pages: Optional[int] = None
|
|
390
|
+
publisher: Optional[Person] = None
|
|
391
|
+
rating: Optional[Rating] = None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class Price(BaseModel):
|
|
395
|
+
price: str
|
|
396
|
+
price_currency: str
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class Article(BaseModel):
|
|
400
|
+
author: Optional[List[Person]] = None
|
|
401
|
+
date: Optional[str] = None
|
|
402
|
+
publisher: Optional["Organization"] = None
|
|
403
|
+
thumbnail: Optional[Thumbnail] = None
|
|
404
|
+
isAccessibleForFree: Optional[bool] = None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class Organization(BaseModel):
|
|
408
|
+
type: Literal["organization"] = "organization"
|
|
409
|
+
contact_points: Optional[List[ContactPoint]] = None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class Software(BaseModel):
|
|
413
|
+
name: Optional[str] = None
|
|
414
|
+
author: Optional[str] = None
|
|
415
|
+
version: Optional[str] = None
|
|
416
|
+
codeRepository: Optional[str] = None
|
|
417
|
+
homepage: Optional[str] = None
|
|
418
|
+
datePublisher: Optional[str] = None
|
|
419
|
+
is_npm: Optional[bool] = None
|
|
420
|
+
is_pypi: Optional[bool] = None
|
|
421
|
+
stars: Optional[int] = None
|
|
422
|
+
forks: Optional[int] = None
|
|
423
|
+
ProgrammingLanguage: Optional[str] = None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class MusicRecording(BaseModel):
|
|
427
|
+
name: str
|
|
428
|
+
thumbnail: Optional[Thumbnail] = None
|
|
429
|
+
rating: Optional[Rating] = None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class MovieData(BaseModel):
|
|
433
|
+
name: Optional[str] = None
|
|
434
|
+
description: Optional[str] = None
|
|
435
|
+
url: Optional[str] = None
|
|
436
|
+
thumbnail: Optional[Thumbnail] = None
|
|
437
|
+
release: Optional[str] = None
|
|
438
|
+
directors: Optional[List[Person]] = None
|
|
439
|
+
actors: Optional[List[Person]] = None
|
|
440
|
+
rating: Optional[Rating] = None
|
|
441
|
+
duration: Optional[str] = None
|
|
442
|
+
genre: Optional[List[str]] = None
|
|
443
|
+
query: Optional[str] = None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class PostalAddress(BaseModel):
|
|
447
|
+
type: Literal["PostalAddress"] = "PostalAddress"
|
|
448
|
+
country: Optional[str] = None
|
|
449
|
+
postalCode: Optional[str] = None
|
|
450
|
+
streetAddress: Optional[str] = None
|
|
451
|
+
addressRegion: Optional[str] = None
|
|
452
|
+
addressLocality: Optional[str] = None
|
|
453
|
+
displayAddress: str
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class DayOpeningHours(BaseModel):
|
|
457
|
+
abbr_name: str
|
|
458
|
+
full_name: str
|
|
459
|
+
opens: str
|
|
460
|
+
closes: str
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class OpeningHours(BaseModel):
|
|
464
|
+
current_day: Optional[List[DayOpeningHours]] = None
|
|
465
|
+
days: Optional[List[List[DayOpeningHours]]] = None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class Reviews(BaseModel):
|
|
469
|
+
results: List["TripAdvisorReview"] # TripAdvisorReview
|
|
470
|
+
viewMoreUrl: str
|
|
471
|
+
reviews_in_foreign_language: bool
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class TripAdvisorReview(BaseModel):
|
|
475
|
+
title: str
|
|
476
|
+
description: str
|
|
477
|
+
date: str
|
|
478
|
+
rating: Rating
|
|
479
|
+
author: Person
|
|
480
|
+
review_url: str
|
|
481
|
+
language: str
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class Action(BaseModel):
|
|
485
|
+
type: str
|
|
486
|
+
url: str
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# Infobox / Graph section
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class AbstractGraphInfobox(Result):
|
|
493
|
+
type: str = "infobox"
|
|
494
|
+
position: int
|
|
495
|
+
label: Optional[str] = None
|
|
496
|
+
category: Optional[str] = None
|
|
497
|
+
long_desc: Optional[str] = None
|
|
498
|
+
thumbnail: Optional[Thumbnail] = None
|
|
499
|
+
attributes: Optional[List[List[str]]] = None
|
|
500
|
+
profiles: Optional[Union[List[Profile], List[DataProvider]]] = None
|
|
501
|
+
website_url: Optional[str] = None
|
|
502
|
+
ratings: Optional[List[Rating]] = None
|
|
503
|
+
providers: Optional[List[DataProvider]] = None
|
|
504
|
+
distance: Optional[Unit] = None
|
|
505
|
+
images: Optional[List[Thumbnail]] = None
|
|
506
|
+
movie: Optional[MovieData] = None
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class GenericInfobox(AbstractGraphInfobox):
|
|
510
|
+
subtype: str = "generic"
|
|
511
|
+
found_in_urls: Optional[List[str]] = None
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class EntityInfobox(AbstractGraphInfobox):
|
|
515
|
+
subtype: Literal["entity"] = "entity"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class QAInfobox(AbstractGraphInfobox):
|
|
519
|
+
subtype: Literal["code"] = "code"
|
|
520
|
+
data: QAPage
|
|
521
|
+
meta_url: Optional[MetaUrl] = None
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class InfoboxWithLocation(AbstractGraphInfobox):
|
|
525
|
+
subtype: Literal["location"] = "location"
|
|
526
|
+
is_location: bool
|
|
527
|
+
coordinates: Optional[List[float]] = None
|
|
528
|
+
zoom_level: int
|
|
529
|
+
location: Optional[LocationResult] = None
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class InfoboxPlace(AbstractGraphInfobox):
|
|
533
|
+
subtype: Literal["place"] = "place"
|
|
534
|
+
location: LocationResult
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class GraphInfobox(BaseModel):
|
|
538
|
+
type: Literal["graph"] = "graph"
|
|
539
|
+
results: List[
|
|
540
|
+
Union[
|
|
541
|
+
GenericInfobox,
|
|
542
|
+
QAInfobox,
|
|
543
|
+
InfoboxPlace,
|
|
544
|
+
InfoboxWithLocation,
|
|
545
|
+
EntityInfobox,
|
|
546
|
+
]
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
class PictureResults(BaseModel):
|
|
551
|
+
viewMoreUrl: Optional[str] = None
|
|
552
|
+
results: List[Thumbnail]
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class Recipe(BaseModel):
|
|
556
|
+
title: str
|
|
557
|
+
description: str
|
|
558
|
+
thumbnail: Thumbnail
|
|
559
|
+
url: str
|
|
560
|
+
domain: str
|
|
561
|
+
favicon: str
|
|
562
|
+
time: Optional[str] = None
|
|
563
|
+
prep_time: Optional[str] = None
|
|
564
|
+
cook_time: Optional[str] = None
|
|
565
|
+
ingredients: Optional[str] = None
|
|
566
|
+
instructions: Optional[List["HowTo"]] = None
|
|
567
|
+
servings: Optional[int] = None
|
|
568
|
+
calories: Optional[int] = None
|
|
569
|
+
rating: Optional[Rating] = None
|
|
570
|
+
recipeCategory: Optional[str] = None
|
|
571
|
+
recipeCuisine: Optional[str] = None
|
|
572
|
+
video: Optional[VideoData] = None
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class HowTo(BaseModel):
|
|
576
|
+
text: str
|
|
577
|
+
name: Optional[str] = None
|
|
578
|
+
url: Optional[str] = None
|
|
579
|
+
image: Optional[List[str]] = None
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class CreativeWork(BaseModel):
|
|
583
|
+
name: str
|
|
584
|
+
thumbnail: Thumbnail
|
|
585
|
+
rating: Optional[Rating] = None
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# Resolve forward references for all relevant models
|
|
589
|
+
SearchResult.model_rebuild()
|
|
590
|
+
DeepResult.model_rebuild()
|
|
591
|
+
LocationResult.model_rebuild()
|
|
592
|
+
Product.model_rebuild()
|
|
593
|
+
Book.model_rebuild()
|
|
594
|
+
Article.model_rebuild()
|
|
595
|
+
Reviews.model_rebuild()
|
|
596
|
+
TripAdvisorReview.model_rebuild()
|
|
597
|
+
WebSearchApiResponse.model_rebuild()
|
|
598
|
+
LocalDescriptionsSearchApiResponse.model_rebuild()
|
|
599
|
+
Recipe.model_rebuild()
|
|
600
|
+
HowTo.model_rebuild()
|
|
601
|
+
CreativeWork.model_rebuild()
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
class WebSearchQueryParams(BaseModel):
|
|
605
|
+
q: str = Field(
|
|
606
|
+
...,
|
|
607
|
+
description="The user’s search query term. Can not be empty. Max 400 chars and 50 words.",
|
|
608
|
+
)
|
|
609
|
+
country: Optional[str] = Field(
|
|
610
|
+
"US",
|
|
611
|
+
max_length=2,
|
|
612
|
+
description="2 character country code. See country codes list.",
|
|
613
|
+
)
|
|
614
|
+
search_lang: Optional[str] = Field(
|
|
615
|
+
"en",
|
|
616
|
+
min_length=2,
|
|
617
|
+
description="2+ character language code. See language codes list.",
|
|
618
|
+
)
|
|
619
|
+
ui_lang: Optional[str] = Field(
|
|
620
|
+
"en-US",
|
|
621
|
+
description="Format <language_code>-<country_code> (see RFC9110 and UI language codes list).",
|
|
622
|
+
)
|
|
623
|
+
count: Optional[int] = Field(
|
|
624
|
+
20, ge=1, le=20, description="Number of search results to return (max 20)."
|
|
625
|
+
)
|
|
626
|
+
offset: Optional[int] = Field(
|
|
627
|
+
0, ge=0, le=9, description="Zero-based page offset (max 9)."
|
|
628
|
+
)
|
|
629
|
+
safesearch: Optional[str] = Field(
|
|
630
|
+
"moderate",
|
|
631
|
+
pattern="^(off|moderate|strict)$",
|
|
632
|
+
description="Adult content filter: off, moderate, strict.",
|
|
633
|
+
)
|
|
634
|
+
freshness: Optional[str] = Field(
|
|
635
|
+
None,
|
|
636
|
+
description=(
|
|
637
|
+
"Limits discovery to a time window. One of: pd (24h), pw (7d), pm (31d), py (365d), "
|
|
638
|
+
"or range YYYY-MM-DDtoYYYY-MM-DD."
|
|
639
|
+
),
|
|
640
|
+
)
|
|
641
|
+
text_decorations: Optional[bool] = Field(
|
|
642
|
+
True,
|
|
643
|
+
description="Whether display strings include decoration markers (highlighting).",
|
|
644
|
+
)
|
|
645
|
+
spellcheck: Optional[bool] = Field(True, description="Spellcheck provided query.")
|
|
646
|
+
result_filter: Optional[str] = Field(
|
|
647
|
+
None,
|
|
648
|
+
description=(
|
|
649
|
+
"Comma delimited result types to include. E.g. discussions,faq,news,web,infobox,query,summarizer,videos,locations."
|
|
650
|
+
),
|
|
651
|
+
)
|
|
652
|
+
goggles_id: Optional[str] = Field(
|
|
653
|
+
None,
|
|
654
|
+
description="(Deprecated) Goggle for custom re-ranking (use `goggles` instead).",
|
|
655
|
+
)
|
|
656
|
+
goggles: Optional[List[str]] = Field(
|
|
657
|
+
None, description="List of goggle URLs/definitions for custom re-ranking."
|
|
658
|
+
)
|
|
659
|
+
units: Optional[str] = Field(
|
|
660
|
+
None,
|
|
661
|
+
pattern="^(metric|imperial)$",
|
|
662
|
+
description="Measurement units: metric, imperial.",
|
|
663
|
+
)
|
|
664
|
+
extra_snippets: Optional[bool] = Field(
|
|
665
|
+
None, description="Get up to 5 additional, alternative result snippets."
|
|
666
|
+
)
|
|
667
|
+
summary: Optional[bool] = Field(
|
|
668
|
+
None, description="Enable summary key generation in web search results."
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
class LocalSearchQueryParams(BaseModel):
|
|
673
|
+
ids: List[str] = Field(
|
|
674
|
+
...,
|
|
675
|
+
min_length=1,
|
|
676
|
+
max_length=20,
|
|
677
|
+
description="List of 1 to 20 non-empty unique location IDs.",
|
|
678
|
+
)
|
|
679
|
+
search_lang: Optional[str] = Field(
|
|
680
|
+
"en",
|
|
681
|
+
min_length=2,
|
|
682
|
+
description="2+ character language code. See language codes list.",
|
|
683
|
+
)
|
|
684
|
+
ui_lang: Optional[str] = Field(
|
|
685
|
+
"en-US",
|
|
686
|
+
description="Format <language_code>-<country_code> (see RFC9110 and UI language codes list).",
|
|
687
|
+
)
|
|
688
|
+
units: Optional[str] = Field(
|
|
689
|
+
None,
|
|
690
|
+
pattern="^(metric|imperial)$",
|
|
691
|
+
description="Measurement units: metric, imperial.",
|
|
692
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
default: install lint test
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv lock --upgrade
|
|
5
|
+
uv sync --all-extras --frozen
|
|
6
|
+
@just hook
|
|
7
|
+
|
|
8
|
+
lint:
|
|
9
|
+
uv run pre-commit run ruff --all-files
|
|
10
|
+
uv run pre-commit run ruff-format --all-files
|
|
11
|
+
uv run pre-commit run mypy --all-files
|
|
12
|
+
uv run pre-commit run end-of-file-fixer --all-files
|
|
13
|
+
uv run pre-commit run mixed-line-ending --all-files
|
|
14
|
+
uv run pre-commit run codespell --all-files
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
test *args:
|
|
18
|
+
uv run --no-sync pytest {{ args }}
|
|
19
|
+
|
|
20
|
+
publish:
|
|
21
|
+
rm -rf dist
|
|
22
|
+
uv build
|
|
23
|
+
uv publish --token $PYPI_TOKEN
|
|
24
|
+
|
|
25
|
+
hook:
|
|
26
|
+
uv run pre-commit install --install-hooks --overwrite
|
|
27
|
+
|
|
28
|
+
unhook:
|
|
29
|
+
uv run pre-commit uninstall
|
|
30
|
+
|
|
31
|
+
docs:
|
|
32
|
+
uv pip install -r docs/requirements.txt
|
|
33
|
+
uv run mkdocs serve
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "brave-api-client"
|
|
3
|
+
description = "Brave API client for Python"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.10,<4"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"httpx>=0.28.1",
|
|
8
|
+
"pydantic>=2.11.5",
|
|
9
|
+
]
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
packages = [
|
|
20
|
+
{ include = "brave_api" },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.mypy]
|
|
29
|
+
plugins = ["pydantic.mypy"]
|
|
30
|
+
python_version = "3.10"
|
|
31
|
+
strict = true
|
|
32
|
+
|
|
33
|
+
[tool.hatch.version]
|
|
34
|
+
source = "vcs"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["brave_api"]
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"mkdocs>=1.6.1",
|
|
42
|
+
"pre-commit>=4.2.0",
|
|
43
|
+
"pytest>=8.3.5",
|
|
44
|
+
"pytest-asyncio>=0.26.0",
|
|
45
|
+
"pytest-cov>=6.1.1",
|
|
46
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from brave_api.web_search.models import WebSearchApiResponse
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_search_response_does_not_validate() -> None:
|
|
8
|
+
with pytest.raises(ValidationError):
|
|
9
|
+
WebSearchApiResponse() # type: ignore[call-arg]
|