Source code for pyfetcher.headers.ua
"""User-Agent generation utilities for :mod:`pyfetcher`.
Purpose:
Provide functions for generating realistic User-Agent strings and
selecting random profiles weighted by real-world browser market share.
Design:
- User-Agent generation leverages the profile system for consistency.
- Random selection uses market-share weights to produce realistic
distributions of browser identities.
- All randomization is done at the function call level, so each call
may produce a different result.
Examples:
::
>>> ua = random_user_agent()
>>> "Mozilla" in ua
True
"""
from __future__ import annotations
import random
from pyfetcher.headers.profiles import (
_PROFILES,
PROFILE_WEIGHTS,
BrowserProfile,
)
[docs]
def random_user_agent(
*,
browser: str | None = None,
platform: str | None = None,
mobile: bool | None = None,
) -> str:
"""Generate a random realistic User-Agent string.
Optionally filter by browser family, platform, or mobile/desktop.
When no filters are specified, profiles are selected using real-world
market share weights.
Args:
browser: Filter to a specific browser family (e.g. ``'chrome'``,
``'firefox'``, ``'safari'``, ``'edge'``). Case-insensitive.
platform: Filter to a specific platform (e.g. ``'Windows'``,
``'macOS'``, ``'Linux'``, ``'Android'``, ``'iOS'``).
Case-insensitive.
mobile: If ``True``, only mobile profiles. If ``False``, only
desktop profiles. If ``None``, any profile.
Returns:
A realistic User-Agent string.
Raises:
ValueError: If no profiles match the given filters.
Examples:
::
>>> ua = random_user_agent(browser="chrome")
>>> "Chrome" in ua
True
>>> ua = random_user_agent(mobile=True)
>>> "Mobile" in ua or "iPhone" in ua
True
"""
profile = random_profile(browser=browser, platform=platform, mobile=mobile)
return profile.user_agent
[docs]
def random_profile(
*,
browser: str | None = None,
platform: str | None = None,
mobile: bool | None = None,
) -> BrowserProfile:
"""Select a random browser profile, optionally filtered.
Profiles are selected using market-share weights when no filters are
applied. When filters narrow the candidate set, weights are renormalized
across matching profiles.
Args:
browser: Filter by browser family (case-insensitive).
platform: Filter by platform name (case-insensitive).
mobile: Filter by mobile/desktop.
Returns:
A randomly selected :class:`BrowserProfile`.
Raises:
ValueError: If no profiles match the given filters.
Examples:
::
>>> profile = random_profile(browser="firefox")
>>> profile.browser
'firefox'
"""
candidates = list(_PROFILES.values())
if browser is not None:
browser_lower = browser.lower()
candidates = [p for p in candidates if p.browser.lower() == browser_lower]
if platform is not None:
platform_lower = platform.lower()
candidates = [p for p in candidates if p.platform.lower() == platform_lower]
if mobile is not None:
candidates = [p for p in candidates if p.mobile == mobile]
if not candidates:
raise ValueError(
f"No profiles match filters: browser={browser!r}, "
f"platform={platform!r}, mobile={mobile!r}"
)
weights = [PROFILE_WEIGHTS.get(p.name, 0.01) for p in candidates]
return random.choices(candidates, weights=weights, k=1)[0] # noqa: S311 # nosec B311
[docs]
def user_agents_for_browser(browser: str) -> list[str]:
"""Return all User-Agent strings for a given browser family.
Args:
browser: Browser family name (case-insensitive).
Returns:
A list of User-Agent strings.
Examples:
::
>>> uas = user_agents_for_browser("chrome")
>>> all("Chrome" in ua for ua in uas)
True
"""
browser_lower = browser.lower()
return [p.user_agent for p in _PROFILES.values() if p.browser.lower() == browser_lower]