Source code for xanax.sources.wallhaven.params

"""
Search parameter handling for the Wallhaven API.

Provides a structured :class:`SearchParams` model with validation
to ensure only valid queries reach the API.
"""

from typing import Any

from pydantic import BaseModel, Field, field_validator

from xanax.errors import ValidationError
from xanax.sources.wallhaven.enums import (
    Category,
    Color,
    FileType,
    Order,
    Purity,
    Ratio,
    Resolution,
    Sort,
    TopRange,
)


[docs] class SearchParams(BaseModel): """ Parameters for a Wallhaven wallpaper search. All fields are validated before any network request is made. Invalid combinations raise :class:`~xanax.errors.ValidationError` immediately. The default ``categories`` includes all three (general, anime, people), matching Wallhaven's own default search behaviour. Example: .. code-block:: python params = SearchParams( query="+anime -sketch", categories=[Category.ANIME], purity=[Purity.SFW], sorting=Sort.TOPLIST, top_range=TopRange.ONE_MONTH, ) """ query: str | None = Field(default=None, description="Search query string") categories: list[Category] = Field( default_factory=lambda: list(Category), description="Categories to search (default: all three)", ) purity: list[Purity] = Field( default_factory=lambda: [Purity.SFW], description="Purity levels to include", ) sorting: Sort = Field(default=Sort.DATE_ADDED, description="How to sort results") order: Order = Field(default=Order.DESC, description="Sort order direction") top_range: TopRange | None = Field(default=None, description="Time range for toplist sorting") resolutions: list[str] = Field( default_factory=list, description="Exact resolutions to filter by (e.g., '1920x1080')", ) ratios: list[str] = Field( default_factory=list, description="Aspect ratios to filter by (e.g., '16x9' or '16:9')", ) colors: list[Color] = Field(default_factory=list, description="Colors to search for") page: int = Field(default=1, ge=1, description="Page number (1-indexed)") seed: str | None = Field( default=None, description="Seed for random sorting (6 alphanumeric characters)" ) file_type: FileType | None = Field(default=None, description="Filter by file type (png or jpg)") like: str | None = Field( default=None, description="Find wallpapers with similar tags to this wallpaper ID", )
[docs] @field_validator("resolutions", mode="before") @classmethod def validate_resolutions(cls, v: list[str] | str | None) -> list[str]: if v is None: return [] if isinstance(v, str): v = [v] for res in v: if not Resolution.validate(res): raise ValidationError( f"Invalid resolution format: {res}. " "Expected format: WIDTHxHEIGHT (e.g., 1920x1080)" ) return v
[docs] @field_validator("ratios", mode="before") @classmethod def validate_ratios(cls, v: list[str] | str | None) -> list[str]: if v is None: return [] if isinstance(v, str): v = [v] for ratio in v: if not Ratio.validate(ratio): raise ValidationError( f"Invalid ratio format: {ratio}. " "Expected format: WIDTH:HEIGHT or WIDTHxHEIGHT (e.g., 16:9 or 16x9)" ) return v
[docs] @field_validator("seed") @classmethod def validate_seed(cls, v: str | None) -> str | None: if v is not None and not (len(v) == 6 and v.isalnum()): raise ValidationError( f"Invalid seed: {v}. Seed must be exactly 6 alphanumeric characters." ) return v
[docs] def model_post_init(self, __context: Any) -> None: if self.top_range is not None and self.sorting != Sort.TOPLIST: raise ValidationError( "top_range can only be used when sorting is set to 'toplist'. " f"Current sorting: {self.sorting.value}" )
[docs] def to_query_params(self) -> dict[str, Any]: """ Serialize to a dict of API query parameters. Returns: Dict suitable for passing as ``params=`` to an httpx request. """ params: dict[str, Any] = {} if self.query: params["q"] = self.query if self.categories: cats = "".join("1" if c in self.categories else "0" for c in Category) params["categories"] = cats if self.purity: purity_str = "".join("1" if p in self.purity else "0" for p in Purity) params["purity"] = purity_str params["sorting"] = self.sorting.value params["order"] = self.order.value if self.top_range: params["topRange"] = self.top_range.value if self.resolutions: params["resolutions"] = ",".join(self.resolutions) if self.ratios: params["ratios"] = ",".join(self.ratios) if self.colors: params["colors"] = ",".join(c.value for c in self.colors) if self.page > 1: params["page"] = self.page if self.seed: params["seed"] = self.seed if self.file_type: params["type"] = self.file_type.value if self.like: params["like"] = self.like return params
[docs] def with_page(self, page: int) -> "SearchParams": """ Return a new :class:`SearchParams` with only the page number changed. Args: page: New page number. Returns: New instance with ``page`` updated and all other fields preserved. """ return SearchParams(**{**self.model_dump(mode="python"), "page": page})
[docs] def with_seed(self, seed: str) -> "SearchParams": """ Return a new :class:`SearchParams` with only the seed changed. Args: seed: New seed value (6 alphanumeric characters). Returns: New instance with ``seed`` updated and all other fields preserved. """ return SearchParams(**{**self.model_dump(mode="python"), "seed": seed})