Skip to content

Commit

Permalink
Merge pull request #18 from kayvane1/staging
Browse files Browse the repository at this point in the history
Adding tests to production
  • Loading branch information
kayvane1 committed Dec 19, 2023
2 parents be33c57 + a3d607f commit cb560cb
Show file tree
Hide file tree
Showing 9 changed files with 678 additions and 325 deletions.
667 changes: 397 additions & 270 deletions poetry.lock

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ generate-setup-file = false


[tool.poetry.dependencies]
python = "^3.8"
requests = "^2.26.0"
httpx = "^0.25.2"
tenacity = "^8.2.3"
pydantic = "^2.5.2"
numpy = "^1.26.2"

pytest-asyncio = "^0.23.2"
numpy = "^1.24.4"

[tool.poetry.group.test]
optional = true
Expand All @@ -31,6 +32,7 @@ optional = true
pytest = "^7.1.1" # Allows for testing of the project
pytest-cov = "^4.0.0" # Allows to run coverage of the project
moto = "^3.1.6" # Allows for mocking of AWS services
coverage = "^7.3.3" # Allows for coverage of the project

[tool.poetry.group.lint]
optional = true
Expand Down Expand Up @@ -145,9 +147,3 @@ addopts = """
testpaths = [
"tests",
]

[tool.coverage.run]
omit = ["*/tests/*"] # Remove test files from coverage run.

[tool.coverage.report]
omit = ["*/tests/*"] # Remove test files from coverage report.
43 changes: 0 additions & 43 deletions src/brave/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,7 @@
from __future__ import annotations

from typing import Any
from typing import Optional
from typing import cast

import httpx


class BraveError(Exception):
"""Base exception class for all Brave Search API errors."""

pass


class APIError(BraveError):
"""Exception raised when the API returns an error response."""

message: str
request: httpx.Request

body: object | None

"""The API response body.
If the API responded with a valid JSON structure then this property will be the
decoded result.
If it isn't a valid JSON structure then this will be the raw response.
If there was no response associated with this error then it will be `None`.
"""

code: Optional[str]
param: Optional[str]
type: Optional[str]

def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None:
super().__init__(message)
self.request = request
self.message = message

if isinstance(body, dict):
self.code = cast(Any, body.get("code"))
self.param = cast(Any, body.get("param"))
self.type = cast(Any, body.get("type"))
else:
self.code = None
self.param = None
self.type = None
2 changes: 1 addition & 1 deletion src/brave/goggles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ Want to learn more about the motivation for Goggles? Check out the [Goggles whit
# This repo will host public goggles contributed by the community and be accessible as part of the package.

- Thought Leadership
- Acamdeic Research
- Academic Research
2 changes: 1 addition & 1 deletion src/brave/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self, api_key: Optional[str] = None, endpoint: str = "web") -> None
super().__init__(api_key=api_key, endpoint=endpoint)

@retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
def _get(self, params: Dict = None) -> requests.Response:
def _get(self, params: Optional[Dict] = None) -> Optional[requests.Response]:
"""
Perform a synchronous GET request to the specified endpoint with optional parameters.
Expand Down
114 changes: 114 additions & 0 deletions tests/test_responses/blue_tack_minimal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"query": {
"original": "Blue tack",
"show_strict_warning": false,
"is_navigational": false,
"is_news_breaking": false,
"local_decision": "drop",
"local_locations_idx": 0,
"spellcheck_off": true,
"country": "us",
"bad_results": false,
"should_fallback": false,
"postal_code": "",
"city": "",
"state": "",
"header_country": "",
"more_results_available": true
},
"mixed": {
"type": "mixed",
"main": [
{
"type": "web",
"index": 0,
"all": false
},
{
"type": "web",
"index": 1,
"all": false
}
],
"top": [],
"side": []
},
"type": "search",
"web": {
"type": "search",
"results": [
{
"title": "Blu Tack - Wikipedia",
"url": "https://en.wikipedia.org/wiki/Blu_Tack",
"is_source_local": false,
"is_source_both": false,
"description": "Blu Tack is a <strong>reusable putty-like pressure-sensitive adhesive produced by Bostik</strong>, commonly used to attach lightweight objects (such as posters or sheets of paper) to walls, doors or other dry surfaces. Traditionally blue, it is also available in other colours.",
"page_age": "2023-11-23T09:26:19",
"language": "en",
"profile": {
"name": "Wikipedia",
"url": "https://en.wikipedia.org/wiki/Blu_Tack",
"long_name": "en.wikipedia.org",
"img": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw"
},
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"deep_results": {},
"meta_url": {
"scheme": "https",
"netloc": "en.wikipedia.org",
"hostname": "en.wikipedia.org",
"favicon": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw",
"path": "› wiki › Blu_Tack"
},
"age": "1 month ago"
}
],
"family_friendly": true
},
"videos": {
"type": "videos",
"results": [
{
"title": "10 Amazing Uses for Blu Tack - YouTube",
"url": "https://www.youtube.com/watch?v=yLsGCdWmqfs",
"description": "Blu Tack has to be one of the World's most versatile products so here are 10 little-known uses for the malleable substance.",
"page_age": "2015-12-02T16:06:16",
"type": "video_result",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": "› watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/iZOYyMN6JdLCMaQtGLzBZF0Rpq_HokKmVlPmZHT7UFY/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS95/THNHQ2RXbXFmcy9o/cWRlZmF1bHQuanBn"
},
"age": "December 2, 2015"
},
{
"title": "Several usages of Blu Tack | How to use Blu Tack | Blu tack uses ...",
"url": "https://www.youtube.com/watch?v=nUoEC1ucl7o",
"description": "In this video I have shown some amazing usages of Blu Tack. Blu Tack is a reusable adhesive and is commonly used to stick light weight objects to the walls, ...",
"page_age": "2018-07-13T10:09:03",
"type": "video_result",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": "› watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/pK3CozSg3Nqqeh0NqSk0qwdVTN488h0RUg-RNZPKu84/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9u/VW9FQzF1Y2w3by9t/YXhyZXNkZWZhdWx0/LmpwZw"
},
"age": "July 13, 2018"
}
],
"mutated_by_goggles": false
}
}
86 changes: 86 additions & 0 deletions tests/unit/test_async_brave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import json

from unittest.mock import AsyncMock

import httpx
import pytest
import tenacity

from brave.async_brave import AsyncBrave
from brave.types import WebSearchApiResponse


@pytest.mark.asyncio
async def test_async_brave_initialization():
client = AsyncBrave(api_key="test_key")
assert client.api_key == "test_key"
assert client.endpoint == "web" # Assuming "web" is the default endpoint


@pytest.mark.asyncio
async def test_async_get_success(monkeypatch):
async def mock_get(*args, **kwargs):
# Create a Mock Response
mock_response = httpx.Response(200, json={"data": "test response"})
# Setting the request attribute
mock_response._request = httpx.Request(method="GET", url=args[0])
return mock_response

monkeypatch.setattr(httpx.AsyncClient, "get", AsyncMock(side_effect=mock_get))

client = AsyncBrave(api_key="test_key")
response = await client._get(params={"q": "test query"})
assert response.json() == {"data": "test response"}


@pytest.mark.asyncio
async def test_async_get_with_retries(monkeypatch):
call_count = 0

async def mock_get(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count < 3:
raise httpx.HTTPError("Temporary failure")
# Create a Mock Response
mock_response = httpx.Response(200, json={"data": "test response after retries"})
# Setting the request attribute
mock_response._request = httpx.Request(method="GET", url=args[0])
return mock_response

monkeypatch.setattr(httpx.AsyncClient, "get", AsyncMock(side_effect=mock_get))

client = AsyncBrave(api_key="test_key")
response = await client._get(params={"q": "test query"})
assert call_count == 3
assert response.json() == {"data": "test response after retries"}


@pytest.mark.asyncio
async def test_async_get_failure(monkeypatch):
# Mocking httpx.AsyncClient.get to always raise an HTTPError
monkeypatch.setattr(httpx.AsyncClient, "get", AsyncMock(side_effect=httpx.HTTPError("Permanent failure")))

client = AsyncBrave(api_key="test_key")

# Expecting tenacity.RetryError instead of httpx.HTTPError
with pytest.raises(tenacity.RetryError):
await client._get(params={"q": "test query"})


@pytest.mark.asyncio
async def test_response_validation(monkeypatch):

with open("tests/test_responses/blue_tack_minimal.json", "r") as f:
_mock_response = json.load(f)

async def mock_get(*args, **kwargs):
mock_response = httpx.Response(200, json=_mock_response)
mock_response._request = httpx.Request(method="GET", url=args[0])
return mock_response

monkeypatch.setattr(httpx.AsyncClient, "get", AsyncMock(side_effect=mock_get))

client = AsyncBrave(api_key="test_key")
response = await client.search("Blue tack") # Replace with the actual async method
assert isinstance(response, WebSearchApiResponse)
58 changes: 56 additions & 2 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,56 @@
def test_placeholder():
pass
import json

from unittest.mock import patch

import pytest

from brave.client import BraveAPIClient
from brave.exceptions import BraveError
from brave.types import WebSearchApiResponse


with open("tests/test_responses/blue_tack_minimal.json", "r") as f:
_mock_response = json.load(f)

mock_response = WebSearchApiResponse.model_validate(_mock_response)


def test_init_with_explicit_api_key():
api_key = "test_api_key"
client = BraveAPIClient(api_key=api_key)
assert client.api_key == api_key


def test_init_with_env_var_api_key(monkeypatch):
monkeypatch.setenv("BRAVE_API_KEY", "env_api_key")
client = BraveAPIClient()
assert client.api_key == "env_api_key"


def test_init_error_without_api_key(monkeypatch):
monkeypatch.delenv("BRAVE_API_KEY", raising=False)
with pytest.raises(BraveError):
BraveAPIClient(api_key=None)


def test_default_endpoint_on_init():
client = BraveAPIClient(api_key="test_api_key")
assert client.endpoint == "web"


def test_custom_endpoint_on_init():
client = BraveAPIClient(api_key="test_api_key", endpoint="custom_endpoint")
assert client.endpoint == "custom_endpoint"


def test_api_key_none_and_env_var_not_set(monkeypatch):
monkeypatch.delenv("BRAVE_API_KEY", raising=False)
with pytest.raises(BraveError):
BraveAPIClient(api_key=None)


def test_response_type_validation():
with patch.object(BraveAPIClient, "search", return_value=mock_response):
client = BraveAPIClient(api_key="test_api_key")
response = client.search(q="Blue tack")
assert isinstance(response, WebSearchApiResponse)
19 changes: 19 additions & 0 deletions tests/unit/test_sync_brave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from unittest.mock import Mock
from unittest.mock import patch

from brave.sync import Brave


def test_brave_initialization():
client = Brave(api_key="test_key")
assert client.api_key == "test_key"
assert client.endpoint == "web" # Assuming "web" is the default endpoint


def test_sync_get_success():
with patch("requests.get") as mock_get:
mock_get.return_value = Mock(status_code=200, json=lambda: {"data": "test response"})

client = Brave(api_key="test_key")
response = client._get(params={"q": "test query"})
assert response.json() == {"data": "test response"}

0 comments on commit cb560cb

Please sign in to comment.