File size: 4,393 Bytes
8ed954c
19d2204
 
 
8ed954c
 
 
 
 
19d2204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ed954c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19d2204
 
 
 
8ed954c
19d2204
8ed954c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19d2204
 
 
 
 
8ed954c
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import os
import random
import time

import requests
from .logger import get_logger

logger = get_logger(__name__)

_BRAVE_URL = "https://api.search.brave.com/res/v1/web/search"
_MAX_RETRIES = 3


def _brave_request(
    params: dict,
    api_key: str,
    accept: str = "application/json",
) -> requests.Response | None:
    """Send a Brave Search request with retry + exponential backoff on 429.

    A small random jitter (0-2 s) is added before the first attempt to
    spread out concurrent requests from parallel region fan-out.
    """
    headers = {
        "Accept": accept,
        "X-Subscription-Token": api_key,
    }

    time.sleep(random.uniform(0, 2.0))

    for attempt in range(_MAX_RETRIES):
        try:
            resp = requests.get(
                _BRAVE_URL, headers=headers, params=params, timeout=15,
            )
            if resp.status_code == 429:
                wait = (2 ** (attempt + 1)) + random.uniform(0, 1)
                logger.info(
                    "Brave 429 (attempt %d/%d), retrying in %.1fs…",
                    attempt + 1, _MAX_RETRIES, wait,
                )
                time.sleep(wait)
                continue
            resp.raise_for_status()
            return resp
        except requests.exceptions.HTTPError:
            raise
        except requests.exceptions.RequestException as exc:
            if attempt < _MAX_RETRIES - 1:
                wait = (2 ** (attempt + 1)) + random.uniform(0, 1)
                logger.info("Brave network error, retrying in %.1fs: %s", wait, exc)
                time.sleep(wait)
            else:
                raise

    return None


def brave_search(query: str, count: int = 5, freshness: str = "") -> str:
    """Single implementation of Brave Search used across the whole project.

    Args:
        query: The search query string.
        count: Number of results to request (max 20).
        freshness: Optional freshness filter (e.g. ``"pw"`` for past week,
                   ``"pm"`` for past month).

    Returns:
        A newline-joined string of ``"- title: description"`` lines, or an
        error message.
    """
    api_key = os.getenv("BRAVE_API_KEY")
    if not api_key:
        logger.warning("BRAVE_API_KEY not set – search skipped")
        return "No Brave API key found."

    params: dict = {"q": query, "count": count}
    if freshness:
        params["freshness"] = freshness

    try:
        resp = _brave_request(params, api_key)
        if resp is None:
            logger.warning("Brave rate-limit exhausted for query: %s", query)
            return "Rate limit hit – try again shortly."

        results = resp.json().get("web", {}).get("results", [])
        if not results:
            logger.info("Brave returned 0 results for: %s", query)
            return "No results found."

        return "\n".join(
            f"- {r.get('title', '')}: {r.get('description', '')}"
            for r in results
        )

    except requests.exceptions.HTTPError as exc:
        if exc.response is not None and exc.response.status_code == 429:
            logger.warning("Brave rate-limit hit for query: %s", query)
            return "Rate limit hit – try again shortly."
        logger.error("Brave HTTP error: %s", exc)
        return f"Search HTTP error: {exc}"
    except requests.exceptions.RequestException as exc:
        logger.error("Brave network error: %s", exc)
        return f"Search error: {exc}"


def brave_search_raw(query: str, count: int = 10, freshness: str = "pm") -> str:
    """Return the raw combined text of titles + descriptions.

    Useful for regex-based ticker extraction in niche_hunter / scanner.
    """
    api_key = os.getenv("BRAVE_API_KEY")
    if not api_key:
        logger.warning("BRAVE_API_KEY not set – raw search skipped")
        return ""

    params: dict = {"q": query, "count": count}
    if freshness:
        params["freshness"] = freshness

    try:
        resp = _brave_request(params, api_key)
        if resp is None:
            logger.warning("Brave rate-limit exhausted for raw query")
            return ""

        results = resp.json().get("web", {}).get("results", [])
        return " ".join(f"{r['title']} {r['description']}" for r in results)
    except requests.exceptions.RequestException as exc:
        logger.error("Brave raw-search error: %s", exc)
        return ""