fix(tinyfish): apply --line-length 88 ruff format to match CI

CI uses 'ruff format --check --line-length 88'; my prior format pass
used the default line length, leaving several lines unwrapped. No
behavior change — purely whitespace.
This commit is contained in:
Chenlu Ji 2026-06-25 23:31:39 -07:00
parent b9923a7cb7
commit 656cdcb60d
3 changed files with 173 additions and 56 deletions

View file

@ -65,7 +65,9 @@ class TinyfishSearchConfig(BaseSearchConfig):
default_api_base=self.TINYFISH_API_BASE,
)
if not resolved_key:
raise ValueError("TINYFISH_API_KEY is not set. Set `TINYFISH_API_KEY` environment variable.")
raise ValueError(
"TINYFISH_API_KEY is not set. Set `TINYFISH_API_KEY` environment variable."
)
return {**headers, "X-API-Key": resolved_key, "Accept": "application/json"}
def get_complete_url(
@ -75,9 +77,13 @@ class TinyfishSearchConfig(BaseSearchConfig):
data: dict[str, object] | list[dict[str, object]] | None = None,
**kwargs: object,
) -> str:
resolved_base = api_base or get_secret_str("TINYFISH_API_BASE") or self.TINYFISH_API_BASE
resolved_base = (
api_base or get_secret_str("TINYFISH_API_BASE") or self.TINYFISH_API_BASE
)
if isinstance(data, dict) and _TINYFISH_PARAMS_KEY in data:
validated_params = _UrlEncodableParams.validate_python(data[_TINYFISH_PARAMS_KEY])
validated_params = _UrlEncodableParams.validate_python(
data[_TINYFISH_PARAMS_KEY]
)
return f"{resolved_base}?{urlencode(validated_params, doseq=True)}"
return resolved_base
@ -113,7 +119,9 @@ class TinyfishSearchConfig(BaseSearchConfig):
resolved_query = " ".join(query) if isinstance(query, list) else query
try:
domains = _StrList.validate_python(optional_params.get("search_domain_filter"))
domains = _StrList.validate_python(
optional_params.get("search_domain_filter")
)
except (ValidationError, TypeError):
domains = []
if domains:
@ -131,7 +139,9 @@ class TinyfishSearchConfig(BaseSearchConfig):
raw_max = optional_params.get("max_results")
if isinstance(raw_max, (int, float, str)):
try:
self._caller_max_results = max(1, min(int(raw_max), _TINYFISH_RESULT_CAP))
self._caller_max_results = max(
1, min(int(raw_max), _TINYFISH_RESULT_CAP)
)
except (ValueError, TypeError, OverflowError):
# OverflowError covers int(float('inf')) and similar non-finite floats.
verbose_logger.warning(
@ -202,7 +212,9 @@ class TinyfishSearchConfig(BaseSearchConfig):
)
try:
raw_json: object = raw_response.json() # any-ok: httpx Response.json() -> Any
raw_json: object = (
raw_response.json()
) # any-ok: httpx Response.json() -> Any
except json.JSONDecodeError:
raise self._wrap_error(
error_message=f"Expected JSON response, got: {raw_response.text[:200]}",
@ -226,7 +238,9 @@ class TinyfishSearchConfig(BaseSearchConfig):
parsed = SearchResponse.model_validate(raw_json)
except ValidationError as e:
raise self._wrap_error(
error_message=(f"Response shape does not match LiteLLM's SearchResponse schema: {e}"),
error_message=(
f"Response shape does not match LiteLLM's SearchResponse schema: {e}"
),
status_code=raw_response.status_code,
headers=dict(raw_response.headers),
)

View file

@ -34,7 +34,9 @@ MOCK_TINYFISH_RESPONSE = {
}
def _make_mock_response(json_data: dict, status_code: int = 200, request_url: str | None = None) -> MagicMock:
def _make_mock_response(
json_data: dict, status_code: int = 200, request_url: str | None = None
) -> MagicMock:
mock = MagicMock()
mock.status_code = status_code
mock.json.return_value = json_data

View file

@ -100,39 +100,53 @@ class TestTinyfishSearchConfig:
class TestTransformSearchRequest:
def test_basic_query(self):
config = TinyfishSearchConfig()
result = config.transform_search_request(query="hello world", optional_params={})
result = config.transform_search_request(
query="hello world", optional_params={}
)
assert result == {"_tinyfish_params": {"query": "hello world"}}
def test_list_query_joined(self):
config = TinyfishSearchConfig()
result = config.transform_search_request(query=["hello", "world"], optional_params={})
result = config.transform_search_request(
query=["hello", "world"], optional_params={}
)
assert result["_tinyfish_params"]["query"] == "hello world"
def test_country_maps_to_location(self):
config = TinyfishSearchConfig()
result = config.transform_search_request(query="test", optional_params={"country": "US"})
result = config.transform_search_request(
query="test", optional_params={"country": "US"}
)
assert result["_tinyfish_params"]["location"] == "US"
def test_max_results_not_sent_on_wire(self):
# TinyFish doesn't honor max_results server-side; we apply it client-side
# in transform_search_response. The querystring should be free of it.
config = TinyfishSearchConfig()
result = config.transform_search_request(query="test", optional_params={"max_results": 5})
result = config.transform_search_request(
query="test", optional_params={"max_results": 5}
)
assert "max_results" not in result["_tinyfish_params"]
def test_max_results_clamped_upper_stored_on_self(self):
config = TinyfishSearchConfig()
config.transform_search_request(query="test", optional_params={"max_results": 100})
config.transform_search_request(
query="test", optional_params={"max_results": 100}
)
assert config._caller_max_results == 10 # TinyFish's natural cap
def test_max_results_clamped_lower_stored_on_self(self):
config = TinyfishSearchConfig()
config.transform_search_request(query="test", optional_params={"max_results": 0})
config.transform_search_request(
query="test", optional_params={"max_results": 0}
)
assert config._caller_max_results == 1
def test_max_results_normal_stored_on_self(self):
config = TinyfishSearchConfig()
config.transform_search_request(query="test", optional_params={"max_results": 5})
config.transform_search_request(
query="test", optional_params={"max_results": 5}
)
assert config._caller_max_results == 5
def test_max_results_non_numeric_string_warns_and_skips(self, caplog):
@ -140,7 +154,9 @@ class TestTransformSearchRequest:
# via warning and treats the value as if max_results wasn't set.
config = TinyfishSearchConfig()
with caplog.at_level("WARNING"):
result = config.transform_search_request(query="test", optional_params={"max_results": "abc"})
result = config.transform_search_request(
query="test", optional_params={"max_results": "abc"}
)
assert config._caller_max_results is None
assert "max_results" not in result["_tinyfish_params"]
messages = [r.getMessage() for r in caplog.records]
@ -152,7 +168,9 @@ class TestTransformSearchRequest:
# warn-and-ignore behavior as other malformed values.
config = TinyfishSearchConfig()
with caplog.at_level("WARNING"):
result = config.transform_search_request(query="test", optional_params={"max_results": float("inf")})
result = config.transform_search_request(
query="test", optional_params={"max_results": float("inf")}
)
assert config._caller_max_results is None
assert "max_results" not in result["_tinyfish_params"]
messages = [r.getMessage() for r in caplog.records]
@ -171,17 +189,23 @@ class TestTransformSearchRequest:
def test_domain_filter_empty_list_ignored(self):
config = TinyfishSearchConfig()
result = config.transform_search_request(query="test", optional_params={"search_domain_filter": []})
result = config.transform_search_request(
query="test", optional_params={"search_domain_filter": []}
)
assert result["_tinyfish_params"]["query"] == "test"
def test_domain_filter_non_list_ignored(self):
config = TinyfishSearchConfig()
result = config.transform_search_request(query="test", optional_params={"search_domain_filter": "not-a-list"})
result = config.transform_search_request(
query="test", optional_params={"search_domain_filter": "not-a-list"}
)
assert result["_tinyfish_params"]["query"] == "test"
def test_unknown_params_passed_through(self):
config = TinyfishSearchConfig()
result = config.transform_search_request(query="test", optional_params={"language": "en", "page": 2})
result = config.transform_search_request(
query="test", optional_params={"language": "en", "page": 2}
)
params = result["_tinyfish_params"]
assert params["language"] == "en"
assert params["page"] == 2
@ -191,7 +215,9 @@ class TestTransformSearchRequest:
supported = config.get_supported_perplexity_optional_params()
if supported:
param = next(p for p in supported if p != "max_results" and p != "country")
result = config.transform_search_request(query="test", optional_params={param: "value"})
result = config.transform_search_request(
query="test", optional_params={param: "value"}
)
assert param not in result["_tinyfish_params"]
def test_arbitrary_param_passed_through(self):
@ -199,7 +225,9 @@ class TestTransformSearchRequest:
# The passthrough loop should forward it verbatim without LiteLLM needing
# to know about it.
config = TinyfishSearchConfig()
result = config.transform_search_request(query="test", optional_params={"fetch": "{}"})
result = config.transform_search_request(
query="test", optional_params={"fetch": "{}"}
)
assert result["_tinyfish_params"]["fetch"] == "{}"
def test_dict_param_auto_json_encoded(self):
@ -210,15 +238,22 @@ class TestTransformSearchRequest:
query="test",
optional_params={"fetch": {"format": "html", "fetch_path": "fast"}},
)
assert result["_tinyfish_params"]["fetch"] == '{"format":"html","fetch_path":"fast"}'
assert (
result["_tinyfish_params"]["fetch"]
== '{"format":"html","fetch_path":"fast"}'
)
def test_bool_param_serialized_as_lowercase(self):
# urlencode renders Python bool as capitalized "True"/"False"; ux-labs
# rejects those (e.g. include_thumbnail must be literal "true"/"false").
# Normalize before passing through.
config = TinyfishSearchConfig()
true_result = config.transform_search_request(query="test", optional_params={"include_thumbnail": True})
false_result = config.transform_search_request(query="test", optional_params={"include_thumbnail": False})
true_result = config.transform_search_request(
query="test", optional_params={"include_thumbnail": True}
)
false_result = config.transform_search_request(
query="test", optional_params={"include_thumbnail": False}
)
assert true_result["_tinyfish_params"]["include_thumbnail"] == "true"
assert false_result["_tinyfish_params"]["include_thumbnail"] == "false"
@ -226,7 +261,9 @@ class TestTransformSearchRequest:
# If the caller already JSON-encoded, don't re-encode.
config = TinyfishSearchConfig()
already = '{"format":"html"}'
result = config.transform_search_request(query="test", optional_params={"fetch": already})
result = config.transform_search_request(
query="test", optional_params={"fetch": already}
)
assert result["_tinyfish_params"]["fetch"] == already
@ -242,7 +279,9 @@ class TestGetCompleteUrl:
def test_custom_api_base(self):
config = TinyfishSearchConfig()
url = config.get_complete_url(api_base="https://custom.api.tinyfish.ai", optional_params={})
url = config.get_complete_url(
api_base="https://custom.api.tinyfish.ai", optional_params={}
)
assert url == "https://custom.api.tinyfish.ai"
def test_env_api_base(self):
@ -275,7 +314,9 @@ class TestGetCompleteUrl:
"litellm.llms.tinyfish.search.transformation.get_secret_str",
return_value=None,
):
url = config.get_complete_url(api_base=None, optional_params={}, data={"other": "value"})
url = config.get_complete_url(
api_base=None, optional_params={}, data={"other": "value"}
)
assert url == "https://api.search.tinyfish.ai"
def test_data_none(self):
@ -292,17 +333,23 @@ class TestTransformSearchResponse:
def test_basic_response(self):
config = TinyfishSearchConfig()
mock_response = _make_mock_response(MOCK_TINYFISH_RESPONSE)
result = config.transform_search_response(raw_response=mock_response, logging_obj=None)
result = config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
assert result.object == "search"
assert len(result.results) == 2
assert result.results[0].title == "TinyFish - AI Web Automation"
assert result.results[0].url == "https://tinyfish.ai"
assert result.results[0].snippet == "Automate any website with natural language."
assert (
result.results[0].snippet == "Automate any website with natural language."
)
def test_empty_results(self):
config = TinyfishSearchConfig()
mock_response = _make_mock_response({"results": []})
result = config.transform_search_response(raw_response=mock_response, logging_obj=None)
result = config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
assert result.object == "search"
assert len(result.results) == 0
@ -321,7 +368,9 @@ class TestTransformSearchResponse:
]
}
mock_response = _make_mock_response(many_results)
result = config.transform_search_response(raw_response=mock_response, logging_obj=None)
result = config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
assert len(result.results) == 3
assert result.results[0].title == "Result 0"
assert result.results[2].title == "Result 2"
@ -340,7 +389,9 @@ class TestTransformSearchResponse:
]
}
mock_response = _make_mock_response(many_results)
result = config.transform_search_response(raw_response=mock_response, logging_obj=None)
result = config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
assert len(result.results) == 10
def test_missing_required_fields_default_to_empty_string(self):
@ -348,8 +399,12 @@ class TestTransformSearchResponse:
# We default missing/null values to "" so a degraded TinyFish result
# flows through instead of failing the whole call.
config = TinyfishSearchConfig()
mock_response = _make_mock_response({"results": [{}, {"title": None, "url": None, "snippet": None}]})
result = config.transform_search_response(raw_response=mock_response, logging_obj=None)
mock_response = _make_mock_response(
{"results": [{}, {"title": None, "url": None, "snippet": None}]}
)
result = config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
assert len(result.results) == 2
for r in result.results:
assert r.title == ""
@ -359,7 +414,9 @@ class TestTransformSearchResponse:
def test_extra_per_result_fields_surface_as_attributes(self):
config = TinyfishSearchConfig()
mock_response = _make_mock_response(MOCK_TINYFISH_RESPONSE)
result = config.transform_search_response(raw_response=mock_response, logging_obj=None)
result = config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
first = result.results[0]
assert getattr(first, "position", None) == 1
assert getattr(first, "site_name", None) == "tinyfish.ai"
@ -385,7 +442,9 @@ class TestTransformSearchResponse:
]
}
mock_response = _make_mock_response(fetched)
result = config.transform_search_response(raw_response=mock_response, logging_obj=None)
result = config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
first = result.results[0]
fetch_field = getattr(first, "fetch", None)
assert isinstance(fetch_field, dict)
@ -394,7 +453,9 @@ class TestTransformSearchResponse:
def test_no_request_uses_default_max_results(self):
config = TinyfishSearchConfig()
mock_response = _make_mock_response(MOCK_TINYFISH_RESPONSE)
result = config.transform_search_response(raw_response=mock_response, logging_obj=None)
result = config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
assert len(result.results) == 2
def test_parameter_warnings_reader_emits_log_lines(self, caplog):
@ -417,7 +478,9 @@ class TestTransformSearchResponse:
}
mock_response = _make_mock_response(body)
with caplog.at_level("WARNING"):
config.transform_search_response(raw_response=mock_response, logging_obj=None)
config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
messages = [r.getMessage() for r in caplog.records]
assert any("max_tokens_per_page" in m for m in messages)
# The type is included in the message so agents can branch on it.
@ -428,8 +491,12 @@ class TestTransformSearchResponse:
config = TinyfishSearchConfig()
mock_response = _make_mock_response(MOCK_TINYFISH_RESPONSE)
with caplog.at_level("WARNING"):
config.transform_search_response(raw_response=mock_response, logging_obj=None)
assert not any("TinyFish Search ignored" in r.getMessage() for r in caplog.records)
config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
assert not any(
"TinyFish Search ignored" in r.getMessage() for r in caplog.records
)
def test_parameter_warnings_malformed_shapes_never_throw(self):
# Every shape that doesn't match {parameter: str, message: str} should
@ -446,7 +513,9 @@ class TestTransformSearchResponse:
]
for bad_value in malformed_field_values:
body = {"results": good_results, "parameter_warnings": bad_value}
config.transform_search_response(raw_response=_make_mock_response(body), logging_obj=None) # must not raise
config.transform_search_response(
raw_response=_make_mock_response(body), logging_obj=None
) # must not raise
malformed_entries = [
"string in list", # non-dict
@ -455,13 +524,23 @@ class TestTransformSearchResponse:
{"type": "unsupported", "parameter": "x"}, # missing message
{"type": "unsupported", "message": "y"}, # missing parameter
{"parameter": "x", "message": "y"}, # missing type
{"type": "unsupported", "parameter": None, "message": "y"}, # null parameter
{
"type": "unsupported",
"parameter": None,
"message": "y",
}, # null parameter
{"type": "unsupported", "parameter": "x", "message": ""}, # empty message
{"type": "unsupported", "parameter": 42, "message": "y"}, # non-string parameter
{
"type": "unsupported",
"parameter": 42,
"message": "y",
}, # non-string parameter
{"type": 1, "parameter": "x", "message": "y"}, # non-string type
]
body = {"results": good_results, "parameter_warnings": malformed_entries}
config.transform_search_response(raw_response=_make_mock_response(body), logging_obj=None) # must not raise
config.transform_search_response(
raw_response=_make_mock_response(body), logging_obj=None
) # must not raise
def test_parameter_warnings_malformed_entries_emit_nothing(self, caplog):
config = TinyfishSearchConfig()
@ -478,7 +557,9 @@ class TestTransformSearchResponse:
],
}
with caplog.at_level("WARNING"):
config.transform_search_response(raw_response=_make_mock_response(body), logging_obj=None)
config.transform_search_response(
raw_response=_make_mock_response(body), logging_obj=None
)
messages = [r.getMessage() for r in caplog.records]
assert sum("parameter_warning" in m for m in messages) == 1
assert any("valid_one" in m for m in messages)
@ -497,7 +578,9 @@ class TestErrorHandling:
}
mock_response = _make_mock_response(body, status_code=400)
with pytest.raises(Exception) as exc_info:
config.transform_search_response(raw_response=mock_response, logging_obj=None)
config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
msg = str(exc_info.value)
assert "TinyFish Search:" in msg
assert "query is required" in msg
@ -507,9 +590,13 @@ class TestErrorHandling:
def test_429_preserves_status_code_and_headers(self):
config = TinyfishSearchConfig()
body = {"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "60 rpm"}}
mock_response = _make_mock_response(body, status_code=429, headers={"Retry-After": "60"})
mock_response = _make_mock_response(
body, status_code=429, headers={"Retry-After": "60"}
)
with pytest.raises(Exception) as exc_info:
config.transform_search_response(raw_response=mock_response, logging_obj=None)
config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
assert getattr(exc_info.value, "status_code", None) == 429
headers = getattr(exc_info.value, "headers", {}) or {}
assert headers.get("Retry-After") == "60"
@ -520,7 +607,9 @@ class TestErrorHandling:
body = {"errors": [{"code": "10000", "message": "Internal"}]}
mock_response = _make_mock_response(body, status_code=502)
with pytest.raises(Exception) as exc_info:
config.transform_search_response(raw_response=mock_response, logging_obj=None)
config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
msg = str(exc_info.value)
assert "TinyFish Search:" in msg
# The raw JSON body string should appear in the message verbatim.
@ -528,9 +617,13 @@ class TestErrorHandling:
def test_non_json_4xx_body_uses_raw_text(self):
config = TinyfishSearchConfig()
mock_response = _make_mock_response(json_data=None, status_code=502, text="<html>Bad Gateway</html>")
mock_response = _make_mock_response(
json_data=None, status_code=502, text="<html>Bad Gateway</html>"
)
with pytest.raises(Exception) as exc_info:
config.transform_search_response(raw_response=mock_response, logging_obj=None)
config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
msg = str(exc_info.value)
assert "TinyFish Search:" in msg
assert "Bad Gateway" in msg
@ -538,9 +631,13 @@ class TestErrorHandling:
def test_non_json_200_body_routes_through_get_error_class(self):
# 200 but the body isn't JSON (degraded backend, CDN-injected page, etc.)
config = TinyfishSearchConfig()
mock_response = _make_mock_response(json_data=None, status_code=200, text="not json")
mock_response = _make_mock_response(
json_data=None, status_code=200, text="not json"
)
with pytest.raises(Exception) as exc_info:
config.transform_search_response(raw_response=mock_response, logging_obj=None)
config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
msg = str(exc_info.value)
assert "TinyFish Search:" in msg
assert "Expected JSON response" in msg
@ -551,7 +648,9 @@ class TestErrorHandling:
# they hit BaseSearchConfig.get_error_class via LiteLLM core.
config = TinyfishSearchConfig()
body = '{"error": {"code": "UNAUTHORIZED", "message": "bad key"}}'
exc = config._wrap_error(error_message=body, status_code=401, headers={"x": "y"})
exc = config._wrap_error(
error_message=body, status_code=401, headers={"x": "y"}
)
msg = str(exc)
assert "TinyFish Search:" in msg
assert "bad key" in msg
@ -565,7 +664,9 @@ class TestErrorHandling:
config = TinyfishSearchConfig()
mock_response = _make_mock_response({"query": "x"}) # no `results` key
with pytest.raises(Exception) as exc_info:
config.transform_search_response(raw_response=mock_response, logging_obj=None)
config.transform_search_response(
raw_response=mock_response, logging_obj=None
)
msg = str(exc_info.value)
assert "TinyFish Search:" in msg
assert "schema" in msg.lower()