webscout 8.2.9__py3-none-any.whl → 2026.1.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webscout/AIauto.py +524 -251
- webscout/AIbase.py +247 -319
- webscout/AIutel.py +68 -703
- webscout/Bard.py +1072 -1026
- webscout/Extra/GitToolkit/__init__.py +10 -10
- webscout/Extra/GitToolkit/gitapi/__init__.py +20 -12
- webscout/Extra/GitToolkit/gitapi/gist.py +142 -0
- webscout/Extra/GitToolkit/gitapi/organization.py +91 -0
- webscout/Extra/GitToolkit/gitapi/repository.py +308 -195
- webscout/Extra/GitToolkit/gitapi/search.py +162 -0
- webscout/Extra/GitToolkit/gitapi/trending.py +236 -0
- webscout/Extra/GitToolkit/gitapi/user.py +128 -96
- webscout/Extra/GitToolkit/gitapi/utils.py +82 -62
- webscout/Extra/YTToolkit/README.md +443 -375
- webscout/Extra/YTToolkit/YTdownloader.py +953 -957
- webscout/Extra/YTToolkit/__init__.py +3 -3
- webscout/Extra/YTToolkit/transcriber.py +595 -476
- webscout/Extra/YTToolkit/ytapi/README.md +230 -44
- webscout/Extra/YTToolkit/ytapi/__init__.py +22 -6
- webscout/Extra/YTToolkit/ytapi/captions.py +190 -0
- webscout/Extra/YTToolkit/ytapi/channel.py +302 -307
- webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
- webscout/Extra/YTToolkit/ytapi/extras.py +178 -118
- webscout/Extra/YTToolkit/ytapi/hashtag.py +120 -0
- webscout/Extra/YTToolkit/ytapi/https.py +89 -88
- webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
- webscout/Extra/YTToolkit/ytapi/playlist.py +59 -59
- webscout/Extra/YTToolkit/ytapi/pool.py +8 -8
- webscout/Extra/YTToolkit/ytapi/query.py +143 -40
- webscout/Extra/YTToolkit/ytapi/shorts.py +122 -0
- webscout/Extra/YTToolkit/ytapi/stream.py +68 -63
- webscout/Extra/YTToolkit/ytapi/suggestions.py +97 -0
- webscout/Extra/YTToolkit/ytapi/utils.py +66 -62
- webscout/Extra/YTToolkit/ytapi/video.py +403 -232
- webscout/Extra/__init__.py +2 -3
- webscout/Extra/gguf.py +1298 -684
- webscout/Extra/tempmail/README.md +487 -487
- webscout/Extra/tempmail/__init__.py +28 -28
- webscout/Extra/tempmail/async_utils.py +143 -141
- webscout/Extra/tempmail/base.py +172 -161
- webscout/Extra/tempmail/cli.py +191 -187
- webscout/Extra/tempmail/emailnator.py +88 -84
- webscout/Extra/tempmail/mail_tm.py +378 -361
- webscout/Extra/tempmail/temp_mail_io.py +304 -292
- webscout/Extra/weather.py +196 -194
- webscout/Extra/weather_ascii.py +17 -15
- webscout/Provider/AISEARCH/PERPLEXED_search.py +175 -0
- webscout/Provider/AISEARCH/Perplexity.py +292 -333
- webscout/Provider/AISEARCH/README.md +106 -279
- webscout/Provider/AISEARCH/__init__.py +16 -9
- webscout/Provider/AISEARCH/brave_search.py +298 -0
- webscout/Provider/AISEARCH/iask_search.py +357 -410
- webscout/Provider/AISEARCH/monica_search.py +200 -220
- webscout/Provider/AISEARCH/webpilotai_search.py +242 -255
- webscout/Provider/Algion.py +413 -0
- webscout/Provider/Andi.py +74 -69
- webscout/Provider/Apriel.py +313 -0
- webscout/Provider/Ayle.py +323 -0
- webscout/Provider/ChatSandbox.py +329 -342
- webscout/Provider/ClaudeOnline.py +365 -0
- webscout/Provider/Cohere.py +232 -208
- webscout/Provider/DeepAI.py +367 -0
- webscout/Provider/Deepinfra.py +467 -340
- webscout/Provider/EssentialAI.py +217 -0
- webscout/Provider/ExaAI.py +274 -261
- webscout/Provider/Gemini.py +175 -169
- webscout/Provider/GithubChat.py +385 -369
- webscout/Provider/Gradient.py +286 -0
- webscout/Provider/Groq.py +556 -801
- webscout/Provider/HadadXYZ.py +323 -0
- webscout/Provider/HeckAI.py +392 -375
- webscout/Provider/HuggingFace.py +387 -0
- webscout/Provider/IBM.py +340 -0
- webscout/Provider/Jadve.py +317 -291
- webscout/Provider/K2Think.py +306 -0
- webscout/Provider/Koboldai.py +221 -384
- webscout/Provider/Netwrck.py +273 -270
- webscout/Provider/Nvidia.py +310 -0
- webscout/Provider/OPENAI/DeepAI.py +489 -0
- webscout/Provider/OPENAI/K2Think.py +423 -0
- webscout/Provider/OPENAI/PI.py +463 -0
- webscout/Provider/OPENAI/README.md +890 -952
- webscout/Provider/OPENAI/TogetherAI.py +405 -0
- webscout/Provider/OPENAI/TwoAI.py +255 -357
- webscout/Provider/OPENAI/__init__.py +148 -40
- webscout/Provider/OPENAI/ai4chat.py +348 -293
- webscout/Provider/OPENAI/akashgpt.py +436 -0
- webscout/Provider/OPENAI/algion.py +303 -0
- webscout/Provider/OPENAI/{exachat.py → ayle.py} +365 -444
- webscout/Provider/OPENAI/base.py +253 -249
- webscout/Provider/OPENAI/cerebras.py +296 -0
- webscout/Provider/OPENAI/chatgpt.py +870 -556
- webscout/Provider/OPENAI/chatsandbox.py +233 -173
- webscout/Provider/OPENAI/deepinfra.py +403 -322
- webscout/Provider/OPENAI/e2b.py +2370 -1414
- webscout/Provider/OPENAI/elmo.py +278 -0
- webscout/Provider/OPENAI/exaai.py +452 -417
- webscout/Provider/OPENAI/freeassist.py +446 -0
- webscout/Provider/OPENAI/gradient.py +448 -0
- webscout/Provider/OPENAI/groq.py +380 -364
- webscout/Provider/OPENAI/hadadxyz.py +292 -0
- webscout/Provider/OPENAI/heckai.py +333 -308
- webscout/Provider/OPENAI/huggingface.py +321 -0
- webscout/Provider/OPENAI/ibm.py +425 -0
- webscout/Provider/OPENAI/llmchat.py +253 -0
- webscout/Provider/OPENAI/llmchatco.py +378 -335
- webscout/Provider/OPENAI/meta.py +541 -0
- webscout/Provider/OPENAI/netwrck.py +374 -357
- webscout/Provider/OPENAI/nvidia.py +317 -0
- webscout/Provider/OPENAI/oivscode.py +348 -287
- webscout/Provider/OPENAI/openrouter.py +328 -0
- webscout/Provider/OPENAI/pydantic_imports.py +1 -172
- webscout/Provider/OPENAI/sambanova.py +397 -0
- webscout/Provider/OPENAI/sonus.py +305 -304
- webscout/Provider/OPENAI/textpollinations.py +370 -339
- webscout/Provider/OPENAI/toolbaz.py +375 -413
- webscout/Provider/OPENAI/typefully.py +419 -355
- webscout/Provider/OPENAI/typliai.py +279 -0
- webscout/Provider/OPENAI/utils.py +314 -318
- webscout/Provider/OPENAI/wisecat.py +359 -387
- webscout/Provider/OPENAI/writecream.py +185 -163
- webscout/Provider/OPENAI/x0gpt.py +462 -365
- webscout/Provider/OPENAI/zenmux.py +380 -0
- webscout/Provider/OpenRouter.py +386 -0
- webscout/Provider/Openai.py +337 -496
- webscout/Provider/PI.py +443 -429
- webscout/Provider/QwenLM.py +346 -254
- webscout/Provider/STT/__init__.py +28 -0
- webscout/Provider/STT/base.py +303 -0
- webscout/Provider/STT/elevenlabs.py +264 -0
- webscout/Provider/Sambanova.py +317 -0
- webscout/Provider/TTI/README.md +69 -82
- webscout/Provider/TTI/__init__.py +37 -7
- webscout/Provider/TTI/base.py +147 -64
- webscout/Provider/TTI/claudeonline.py +393 -0
- webscout/Provider/TTI/magicstudio.py +292 -201
- webscout/Provider/TTI/miragic.py +180 -0
- webscout/Provider/TTI/pollinations.py +331 -221
- webscout/Provider/TTI/together.py +334 -0
- webscout/Provider/TTI/utils.py +14 -11
- webscout/Provider/TTS/README.md +186 -192
- webscout/Provider/TTS/__init__.py +43 -10
- webscout/Provider/TTS/base.py +523 -159
- webscout/Provider/TTS/deepgram.py +286 -156
- webscout/Provider/TTS/elevenlabs.py +189 -111
- webscout/Provider/TTS/freetts.py +218 -0
- webscout/Provider/TTS/murfai.py +288 -113
- webscout/Provider/TTS/openai_fm.py +364 -129
- webscout/Provider/TTS/parler.py +203 -111
- webscout/Provider/TTS/qwen.py +334 -0
- webscout/Provider/TTS/sherpa.py +286 -0
- webscout/Provider/TTS/speechma.py +693 -580
- webscout/Provider/TTS/streamElements.py +275 -333
- webscout/Provider/TTS/utils.py +280 -280
- webscout/Provider/TextPollinationsAI.py +331 -308
- webscout/Provider/TogetherAI.py +450 -0
- webscout/Provider/TwoAI.py +309 -475
- webscout/Provider/TypliAI.py +311 -305
- webscout/Provider/UNFINISHED/ChatHub.py +219 -209
- webscout/Provider/{OPENAI/glider.py → UNFINISHED/ChutesAI.py} +331 -326
- webscout/Provider/{GizAI.py → UNFINISHED/GizAI.py} +300 -295
- webscout/Provider/{Marcus.py → UNFINISHED/Marcus.py} +218 -198
- webscout/Provider/UNFINISHED/Qodo.py +481 -0
- webscout/Provider/{MCPCore.py → UNFINISHED/XenAI.py} +330 -315
- webscout/Provider/UNFINISHED/Youchat.py +347 -330
- webscout/Provider/UNFINISHED/aihumanizer.py +41 -0
- webscout/Provider/UNFINISHED/grammerchecker.py +37 -0
- webscout/Provider/UNFINISHED/liner.py +342 -0
- webscout/Provider/UNFINISHED/liner_api_request.py +246 -263
- webscout/Provider/{samurai.py → UNFINISHED/samurai.py} +231 -224
- webscout/Provider/WiseCat.py +256 -233
- webscout/Provider/WrDoChat.py +390 -370
- webscout/Provider/__init__.py +115 -174
- webscout/Provider/ai4chat.py +181 -174
- webscout/Provider/akashgpt.py +330 -335
- webscout/Provider/cerebras.py +397 -290
- webscout/Provider/cleeai.py +236 -213
- webscout/Provider/elmo.py +291 -283
- webscout/Provider/geminiapi.py +343 -208
- webscout/Provider/julius.py +245 -223
- webscout/Provider/learnfastai.py +333 -325
- webscout/Provider/llama3mitril.py +230 -215
- webscout/Provider/llmchat.py +308 -258
- webscout/Provider/llmchatco.py +321 -306
- webscout/Provider/meta.py +996 -801
- webscout/Provider/oivscode.py +332 -309
- webscout/Provider/searchchat.py +316 -292
- webscout/Provider/sonus.py +264 -258
- webscout/Provider/toolbaz.py +359 -353
- webscout/Provider/turboseek.py +332 -266
- webscout/Provider/typefully.py +262 -202
- webscout/Provider/x0gpt.py +332 -299
- webscout/__init__.py +31 -39
- webscout/__main__.py +5 -5
- webscout/cli.py +585 -524
- webscout/client.py +1497 -70
- webscout/conversation.py +140 -436
- webscout/exceptions.py +383 -362
- webscout/litagent/__init__.py +29 -29
- webscout/litagent/agent.py +492 -455
- webscout/litagent/constants.py +60 -60
- webscout/models.py +505 -181
- webscout/optimizers.py +74 -420
- webscout/prompt_manager.py +376 -288
- webscout/sanitize.py +1514 -0
- webscout/scout/README.md +452 -404
- webscout/scout/__init__.py +8 -8
- webscout/scout/core/__init__.py +7 -7
- webscout/scout/core/crawler.py +330 -210
- webscout/scout/core/scout.py +800 -607
- webscout/scout/core/search_result.py +51 -96
- webscout/scout/core/text_analyzer.py +64 -63
- webscout/scout/core/text_utils.py +412 -277
- webscout/scout/core/web_analyzer.py +54 -52
- webscout/scout/element.py +872 -478
- webscout/scout/parsers/__init__.py +70 -69
- webscout/scout/parsers/html5lib_parser.py +182 -172
- webscout/scout/parsers/html_parser.py +238 -236
- webscout/scout/parsers/lxml_parser.py +203 -178
- webscout/scout/utils.py +38 -37
- webscout/search/__init__.py +47 -0
- webscout/search/base.py +201 -0
- webscout/search/bing_main.py +45 -0
- webscout/search/brave_main.py +92 -0
- webscout/search/duckduckgo_main.py +57 -0
- webscout/search/engines/__init__.py +127 -0
- webscout/search/engines/bing/__init__.py +15 -0
- webscout/search/engines/bing/base.py +35 -0
- webscout/search/engines/bing/images.py +114 -0
- webscout/search/engines/bing/news.py +96 -0
- webscout/search/engines/bing/suggestions.py +36 -0
- webscout/search/engines/bing/text.py +109 -0
- webscout/search/engines/brave/__init__.py +19 -0
- webscout/search/engines/brave/base.py +47 -0
- webscout/search/engines/brave/images.py +213 -0
- webscout/search/engines/brave/news.py +353 -0
- webscout/search/engines/brave/suggestions.py +318 -0
- webscout/search/engines/brave/text.py +167 -0
- webscout/search/engines/brave/videos.py +364 -0
- webscout/search/engines/duckduckgo/__init__.py +25 -0
- webscout/search/engines/duckduckgo/answers.py +80 -0
- webscout/search/engines/duckduckgo/base.py +189 -0
- webscout/search/engines/duckduckgo/images.py +100 -0
- webscout/search/engines/duckduckgo/maps.py +183 -0
- webscout/search/engines/duckduckgo/news.py +70 -0
- webscout/search/engines/duckduckgo/suggestions.py +22 -0
- webscout/search/engines/duckduckgo/text.py +221 -0
- webscout/search/engines/duckduckgo/translate.py +48 -0
- webscout/search/engines/duckduckgo/videos.py +80 -0
- webscout/search/engines/duckduckgo/weather.py +84 -0
- webscout/search/engines/mojeek.py +61 -0
- webscout/search/engines/wikipedia.py +77 -0
- webscout/search/engines/yahoo/__init__.py +41 -0
- webscout/search/engines/yahoo/answers.py +19 -0
- webscout/search/engines/yahoo/base.py +34 -0
- webscout/search/engines/yahoo/images.py +323 -0
- webscout/search/engines/yahoo/maps.py +19 -0
- webscout/search/engines/yahoo/news.py +258 -0
- webscout/search/engines/yahoo/suggestions.py +140 -0
- webscout/search/engines/yahoo/text.py +273 -0
- webscout/search/engines/yahoo/translate.py +19 -0
- webscout/search/engines/yahoo/videos.py +302 -0
- webscout/search/engines/yahoo/weather.py +220 -0
- webscout/search/engines/yandex.py +67 -0
- webscout/search/engines/yep/__init__.py +13 -0
- webscout/search/engines/yep/base.py +34 -0
- webscout/search/engines/yep/images.py +101 -0
- webscout/search/engines/yep/suggestions.py +38 -0
- webscout/search/engines/yep/text.py +99 -0
- webscout/search/http_client.py +172 -0
- webscout/search/results.py +141 -0
- webscout/search/yahoo_main.py +57 -0
- webscout/search/yep_main.py +48 -0
- webscout/server/__init__.py +48 -0
- webscout/server/config.py +78 -0
- webscout/server/exceptions.py +69 -0
- webscout/server/providers.py +286 -0
- webscout/server/request_models.py +131 -0
- webscout/server/request_processing.py +404 -0
- webscout/server/routes.py +642 -0
- webscout/server/server.py +351 -0
- webscout/server/ui_templates.py +1171 -0
- webscout/swiftcli/__init__.py +79 -95
- webscout/swiftcli/core/__init__.py +7 -7
- webscout/swiftcli/core/cli.py +574 -297
- webscout/swiftcli/core/context.py +98 -104
- webscout/swiftcli/core/group.py +268 -241
- webscout/swiftcli/decorators/__init__.py +28 -28
- webscout/swiftcli/decorators/command.py +243 -221
- webscout/swiftcli/decorators/options.py +247 -220
- webscout/swiftcli/decorators/output.py +392 -252
- webscout/swiftcli/exceptions.py +21 -21
- webscout/swiftcli/plugins/__init__.py +9 -9
- webscout/swiftcli/plugins/base.py +134 -135
- webscout/swiftcli/plugins/manager.py +269 -269
- webscout/swiftcli/utils/__init__.py +58 -59
- webscout/swiftcli/utils/formatting.py +251 -252
- webscout/swiftcli/utils/parsing.py +368 -267
- webscout/update_checker.py +280 -136
- webscout/utils.py +28 -14
- webscout/version.py +2 -1
- webscout/version.py.bak +3 -0
- webscout/zeroart/__init__.py +218 -135
- webscout/zeroart/base.py +70 -66
- webscout/zeroart/effects.py +155 -101
- webscout/zeroart/fonts.py +1799 -1239
- webscout-2026.1.19.dist-info/METADATA +638 -0
- webscout-2026.1.19.dist-info/RECORD +312 -0
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/WHEEL +1 -1
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/entry_points.txt +1 -1
- webscout/DWEBS.py +0 -520
- webscout/Extra/Act.md +0 -309
- webscout/Extra/GitToolkit/gitapi/README.md +0 -110
- webscout/Extra/autocoder/__init__.py +0 -9
- webscout/Extra/autocoder/autocoder.py +0 -1105
- webscout/Extra/autocoder/autocoder_utiles.py +0 -332
- webscout/Extra/gguf.md +0 -430
- webscout/Extra/weather.md +0 -281
- webscout/Litlogger/README.md +0 -10
- webscout/Litlogger/__init__.py +0 -15
- webscout/Litlogger/formats.py +0 -4
- webscout/Litlogger/handlers.py +0 -103
- webscout/Litlogger/levels.py +0 -13
- webscout/Litlogger/logger.py +0 -92
- webscout/Provider/AI21.py +0 -177
- webscout/Provider/AISEARCH/DeepFind.py +0 -254
- webscout/Provider/AISEARCH/felo_search.py +0 -202
- webscout/Provider/AISEARCH/genspark_search.py +0 -324
- webscout/Provider/AISEARCH/hika_search.py +0 -186
- webscout/Provider/AISEARCH/scira_search.py +0 -298
- webscout/Provider/Aitopia.py +0 -316
- webscout/Provider/AllenAI.py +0 -440
- webscout/Provider/Blackboxai.py +0 -791
- webscout/Provider/ChatGPTClone.py +0 -237
- webscout/Provider/ChatGPTGratis.py +0 -194
- webscout/Provider/Cloudflare.py +0 -324
- webscout/Provider/ExaChat.py +0 -358
- webscout/Provider/Flowith.py +0 -217
- webscout/Provider/FreeGemini.py +0 -250
- webscout/Provider/Glider.py +0 -225
- webscout/Provider/HF_space/__init__.py +0 -0
- webscout/Provider/HF_space/qwen_qwen2.py +0 -206
- webscout/Provider/HuggingFaceChat.py +0 -469
- webscout/Provider/Hunyuan.py +0 -283
- webscout/Provider/LambdaChat.py +0 -411
- webscout/Provider/Llama3.py +0 -259
- webscout/Provider/Nemotron.py +0 -218
- webscout/Provider/OLLAMA.py +0 -396
- webscout/Provider/OPENAI/BLACKBOXAI.py +0 -766
- webscout/Provider/OPENAI/Cloudflare.py +0 -378
- webscout/Provider/OPENAI/FreeGemini.py +0 -283
- webscout/Provider/OPENAI/NEMOTRON.py +0 -232
- webscout/Provider/OPENAI/Qwen3.py +0 -283
- webscout/Provider/OPENAI/api.py +0 -969
- webscout/Provider/OPENAI/c4ai.py +0 -373
- webscout/Provider/OPENAI/chatgptclone.py +0 -494
- webscout/Provider/OPENAI/copilot.py +0 -242
- webscout/Provider/OPENAI/flowith.py +0 -162
- webscout/Provider/OPENAI/freeaichat.py +0 -359
- webscout/Provider/OPENAI/mcpcore.py +0 -389
- webscout/Provider/OPENAI/multichat.py +0 -376
- webscout/Provider/OPENAI/opkfc.py +0 -496
- webscout/Provider/OPENAI/scirachat.py +0 -477
- webscout/Provider/OPENAI/standardinput.py +0 -433
- webscout/Provider/OPENAI/typegpt.py +0 -364
- webscout/Provider/OPENAI/uncovrAI.py +0 -463
- webscout/Provider/OPENAI/venice.py +0 -431
- webscout/Provider/OPENAI/yep.py +0 -382
- webscout/Provider/OpenGPT.py +0 -209
- webscout/Provider/Perplexitylabs.py +0 -415
- webscout/Provider/Reka.py +0 -214
- webscout/Provider/StandardInput.py +0 -290
- webscout/Provider/TTI/aiarta.py +0 -365
- webscout/Provider/TTI/artbit.py +0 -0
- webscout/Provider/TTI/fastflux.py +0 -200
- webscout/Provider/TTI/piclumen.py +0 -203
- webscout/Provider/TTI/pixelmuse.py +0 -225
- webscout/Provider/TTS/gesserit.py +0 -128
- webscout/Provider/TTS/sthir.py +0 -94
- webscout/Provider/TeachAnything.py +0 -229
- webscout/Provider/UNFINISHED/puterjs.py +0 -635
- webscout/Provider/UNFINISHED/test_lmarena.py +0 -119
- webscout/Provider/Venice.py +0 -258
- webscout/Provider/VercelAI.py +0 -253
- webscout/Provider/Writecream.py +0 -246
- webscout/Provider/WritingMate.py +0 -269
- webscout/Provider/asksteve.py +0 -220
- webscout/Provider/chatglm.py +0 -215
- webscout/Provider/copilot.py +0 -425
- webscout/Provider/freeaichat.py +0 -285
- webscout/Provider/granite.py +0 -235
- webscout/Provider/hermes.py +0 -266
- webscout/Provider/koala.py +0 -170
- webscout/Provider/lmarena.py +0 -198
- webscout/Provider/multichat.py +0 -364
- webscout/Provider/scira_chat.py +0 -299
- webscout/Provider/scnet.py +0 -243
- webscout/Provider/talkai.py +0 -194
- webscout/Provider/typegpt.py +0 -289
- webscout/Provider/uncovr.py +0 -368
- webscout/Provider/yep.py +0 -389
- webscout/litagent/Readme.md +0 -276
- webscout/litprinter/__init__.py +0 -59
- webscout/swiftcli/Readme.md +0 -323
- webscout/tempid.py +0 -128
- webscout/webscout_search.py +0 -1184
- webscout/webscout_search_async.py +0 -654
- webscout/yep_search.py +0 -347
- webscout/zeroart/README.md +0 -89
- webscout-8.2.9.dist-info/METADATA +0 -1033
- webscout-8.2.9.dist-info/RECORD +0 -289
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.2.9.dist-info → webscout-2026.1.19.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Brave news search implementation.
|
|
2
|
+
|
|
3
|
+
This module provides a Brave news search engine that parses HTML responses
|
|
4
|
+
from Brave Search to extract news article results.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from webscout.search.engines.brave.news import BraveNews
|
|
8
|
+
>>> searcher = BraveNews()
|
|
9
|
+
>>> results = searcher.run("technology news", max_results=10)
|
|
10
|
+
>>> for article in results:
|
|
11
|
+
... print(f"{article.title} - {article.source}")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from time import sleep
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from webscout.scout import Scout
|
|
20
|
+
|
|
21
|
+
from ....search.results import NewsResult
|
|
22
|
+
from .base import BraveBase
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BraveNews(BraveBase):
|
|
26
|
+
"""Brave news search engine.
|
|
27
|
+
|
|
28
|
+
Searches Brave News Search and parses HTML responses to extract
|
|
29
|
+
news article results including title, URL, source, date, description,
|
|
30
|
+
and thumbnail image.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
name: Engine identifier name.
|
|
34
|
+
provider: Provider identifier.
|
|
35
|
+
category: Search category type.
|
|
36
|
+
search_url: Base URL for news search.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name = "brave_news"
|
|
40
|
+
provider = "brave"
|
|
41
|
+
category = "news"
|
|
42
|
+
|
|
43
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
44
|
+
"""Initialize Brave news search client.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
*args: Positional arguments passed to BraveBase.
|
|
48
|
+
**kwargs: Keyword arguments passed to BraveBase.
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(*args, **kwargs)
|
|
51
|
+
self.search_url = f"{self.base_url}/news"
|
|
52
|
+
|
|
53
|
+
def run(self, *args: Any, **kwargs: Any) -> list[NewsResult]:
|
|
54
|
+
"""Run news search on Brave.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
*args: Positional arguments. First arg is the search query.
|
|
58
|
+
**kwargs: Keyword arguments including:
|
|
59
|
+
- keywords: Search query string.
|
|
60
|
+
- region: Region code (e.g., 'us-en').
|
|
61
|
+
- safesearch: Safe search level ('on', 'moderate', 'off').
|
|
62
|
+
- max_results: Maximum number of results to return.
|
|
63
|
+
- timelimit: Time filter for results ('d' for day, 'w' for week, etc.).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of NewsResult objects containing news article information.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If no keywords are provided.
|
|
70
|
+
Exception: If the HTTP request fails.
|
|
71
|
+
"""
|
|
72
|
+
keywords = args[0] if args else kwargs.get("keywords")
|
|
73
|
+
region = args[1] if len(args) > 1 else kwargs.get("region", "us-en")
|
|
74
|
+
safesearch = args[2] if len(args) > 2 else kwargs.get("safesearch", "moderate")
|
|
75
|
+
max_results = args[3] if len(args) > 3 else kwargs.get("max_results", 10)
|
|
76
|
+
timelimit = kwargs.get("timelimit")
|
|
77
|
+
|
|
78
|
+
if max_results is None:
|
|
79
|
+
max_results = 10
|
|
80
|
+
|
|
81
|
+
if not keywords:
|
|
82
|
+
raise ValueError("Keywords are mandatory")
|
|
83
|
+
|
|
84
|
+
safesearch_map = {"on": "strict", "moderate": "moderate", "off": "off"}
|
|
85
|
+
safesearch_str = str(safesearch).lower() if safesearch else "moderate"
|
|
86
|
+
safesearch_value = safesearch_map.get(safesearch_str, "moderate")
|
|
87
|
+
|
|
88
|
+
fetched_results: list[NewsResult] = []
|
|
89
|
+
fetched_urls: set[str] = set()
|
|
90
|
+
|
|
91
|
+
offset = 0
|
|
92
|
+
while len(fetched_results) < max_results:
|
|
93
|
+
params: dict[str, str] = {
|
|
94
|
+
"q": keywords,
|
|
95
|
+
"source": "web",
|
|
96
|
+
"safesearch": safesearch_value,
|
|
97
|
+
"spellcheck": "0",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if offset > 0:
|
|
101
|
+
params["offset"] = str(offset)
|
|
102
|
+
|
|
103
|
+
if timelimit:
|
|
104
|
+
params["tf"] = timelimit
|
|
105
|
+
|
|
106
|
+
if region:
|
|
107
|
+
params["region"] = region
|
|
108
|
+
|
|
109
|
+
html = self._fetch_page(params)
|
|
110
|
+
page_results = self._parse_results_from_html(html)
|
|
111
|
+
|
|
112
|
+
if not page_results:
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
for result in page_results:
|
|
116
|
+
if len(fetched_results) >= max_results:
|
|
117
|
+
break
|
|
118
|
+
if result.url and result.url not in fetched_urls:
|
|
119
|
+
fetched_urls.add(result.url)
|
|
120
|
+
fetched_results.append(result)
|
|
121
|
+
|
|
122
|
+
offset += 1
|
|
123
|
+
|
|
124
|
+
if self.sleep_interval:
|
|
125
|
+
sleep(self.sleep_interval)
|
|
126
|
+
|
|
127
|
+
return fetched_results[:max_results]
|
|
128
|
+
|
|
129
|
+
def _fetch_page(self, params: dict[str, str]) -> str:
|
|
130
|
+
"""Fetch HTML page from Brave news search.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
params: Query parameters for the request.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
HTML content of the response.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
Exception: If the request fails after retries.
|
|
140
|
+
"""
|
|
141
|
+
headers = dict(self.session.headers) if getattr(self, "session", None) else {}
|
|
142
|
+
headers.update({
|
|
143
|
+
"Referer": "https://search.brave.com/",
|
|
144
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
145
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
attempts = 3
|
|
149
|
+
backoff = 1.0
|
|
150
|
+
last_exc: Exception | None = None
|
|
151
|
+
|
|
152
|
+
for attempt in range(attempts):
|
|
153
|
+
try:
|
|
154
|
+
resp = self.session.get(
|
|
155
|
+
self.search_url,
|
|
156
|
+
params=params,
|
|
157
|
+
headers=headers,
|
|
158
|
+
timeout=self.timeout,
|
|
159
|
+
)
|
|
160
|
+
resp.raise_for_status()
|
|
161
|
+
return resp.text
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
last_exc = exc
|
|
164
|
+
try:
|
|
165
|
+
code = getattr(exc, "code", None) or getattr(exc, "status_code", None)
|
|
166
|
+
except Exception:
|
|
167
|
+
code = None
|
|
168
|
+
if code in (429, 500, 502, 503, 504):
|
|
169
|
+
sleep(backoff)
|
|
170
|
+
backoff *= 2
|
|
171
|
+
continue
|
|
172
|
+
raise Exception(f"Failed to GET {self.search_url}: {exc}") from exc
|
|
173
|
+
|
|
174
|
+
raise Exception(f"Failed to GET {self.search_url} after retries: {last_exc}") from last_exc
|
|
175
|
+
|
|
176
|
+
def _parse_results_from_html(self, html: str) -> list[NewsResult]:
|
|
177
|
+
"""Parse HTML and extract news search results.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
html: Raw HTML content from Brave news search.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of NewsResult objects parsed from the HTML.
|
|
184
|
+
"""
|
|
185
|
+
soup = Scout(html)
|
|
186
|
+
results: list[NewsResult] = []
|
|
187
|
+
|
|
188
|
+
# News results are in div.snippet containers with data-type="news"
|
|
189
|
+
containers = soup.select('div.snippet[data-type="news"]')
|
|
190
|
+
|
|
191
|
+
# Fallback: try generic snippet containers in main
|
|
192
|
+
if not containers:
|
|
193
|
+
main = soup.select_one("main")
|
|
194
|
+
if main:
|
|
195
|
+
containers = main.select("div.snippet")
|
|
196
|
+
|
|
197
|
+
for container in containers:
|
|
198
|
+
try:
|
|
199
|
+
result = self._parse_news_container(container)
|
|
200
|
+
if result and result.url:
|
|
201
|
+
results.append(result)
|
|
202
|
+
except Exception:
|
|
203
|
+
# Skip malformed results
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
return results
|
|
207
|
+
|
|
208
|
+
def _parse_news_container(self, container: Any) -> NewsResult | None:
|
|
209
|
+
"""Parse a single news container element.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
container: Scout element representing a news result container.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
NewsResult object or None if parsing fails.
|
|
216
|
+
"""
|
|
217
|
+
# Get article URL from result header link
|
|
218
|
+
url = ""
|
|
219
|
+
link_elem = container.select_one("a.result-header")
|
|
220
|
+
if link_elem:
|
|
221
|
+
url = link_elem.get("href", "").strip()
|
|
222
|
+
|
|
223
|
+
if not url:
|
|
224
|
+
# Try alternate link location
|
|
225
|
+
link_elem = container.select_one("a[href]")
|
|
226
|
+
if link_elem:
|
|
227
|
+
url = link_elem.get("href", "").strip()
|
|
228
|
+
|
|
229
|
+
if not url:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
# Get title
|
|
233
|
+
title = ""
|
|
234
|
+
title_elem = container.select_one(".snippet-title")
|
|
235
|
+
if title_elem:
|
|
236
|
+
title = title_elem.get_text(strip=True)
|
|
237
|
+
|
|
238
|
+
# Get source (publisher)
|
|
239
|
+
source = ""
|
|
240
|
+
source_elem = container.select_one(".netloc")
|
|
241
|
+
if source_elem:
|
|
242
|
+
source = source_elem.get_text(strip=True)
|
|
243
|
+
|
|
244
|
+
# Get publication date
|
|
245
|
+
date = ""
|
|
246
|
+
# Date is typically in a span.attr after the separator
|
|
247
|
+
cite_elem = container.select_one("cite.snippet-url")
|
|
248
|
+
if cite_elem:
|
|
249
|
+
# Look for date patterns (e.g., "5 hours ago", "1 day ago", "January 15, 2026")
|
|
250
|
+
attr_elems = cite_elem.select(".attr")
|
|
251
|
+
for elem in attr_elems:
|
|
252
|
+
text = elem.get_text(strip=True)
|
|
253
|
+
# Skip separators
|
|
254
|
+
if text == "•":
|
|
255
|
+
continue
|
|
256
|
+
# Check if this looks like a date
|
|
257
|
+
if self._is_date_text(text):
|
|
258
|
+
date = text
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
# Get description
|
|
262
|
+
body = ""
|
|
263
|
+
desc_elem = container.select_one("p.desc, .snippet-description")
|
|
264
|
+
if desc_elem:
|
|
265
|
+
body = desc_elem.get_text(strip=True)
|
|
266
|
+
|
|
267
|
+
# Get thumbnail image
|
|
268
|
+
image = ""
|
|
269
|
+
img_elem = container.select_one(".image-wrapper img, img")
|
|
270
|
+
if img_elem:
|
|
271
|
+
image = img_elem.get("src", "").strip()
|
|
272
|
+
# Skip favicon images
|
|
273
|
+
if "favicon" in image.lower() or "size-xs" in (img_elem.get("class") or ""):
|
|
274
|
+
image = ""
|
|
275
|
+
|
|
276
|
+
return NewsResult(
|
|
277
|
+
title=title,
|
|
278
|
+
url=url,
|
|
279
|
+
source=source,
|
|
280
|
+
date=date,
|
|
281
|
+
body=body,
|
|
282
|
+
image=image,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _is_date_text(self, text: str) -> bool:
|
|
286
|
+
"""Check if text appears to be a date.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
text: Text string to check.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
True if text looks like a date, False otherwise.
|
|
293
|
+
"""
|
|
294
|
+
text_lower = text.lower()
|
|
295
|
+
|
|
296
|
+
# Check for relative time patterns
|
|
297
|
+
relative_patterns = [
|
|
298
|
+
"hour", "hours", "minute", "minutes", "second", "seconds",
|
|
299
|
+
"day", "days", "week", "weeks", "month", "months", "year", "years",
|
|
300
|
+
"ago", "yesterday", "today",
|
|
301
|
+
]
|
|
302
|
+
if any(pattern in text_lower for pattern in relative_patterns):
|
|
303
|
+
return True
|
|
304
|
+
|
|
305
|
+
# Check for month names
|
|
306
|
+
months = [
|
|
307
|
+
"january", "february", "march", "april", "may", "june",
|
|
308
|
+
"july", "august", "september", "october", "november", "december",
|
|
309
|
+
"jan", "feb", "mar", "apr", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
|
|
310
|
+
]
|
|
311
|
+
if any(month in text_lower for month in months):
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
def extract_results(self, html_text: str) -> list[NewsResult]:
|
|
317
|
+
"""Extract news results from HTML text.
|
|
318
|
+
|
|
319
|
+
This is an alias for _parse_results_from_html for API consistency.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
html_text: Raw HTML content from Brave news search.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
List of NewsResult objects parsed from the HTML.
|
|
326
|
+
"""
|
|
327
|
+
return self._parse_results_from_html(html_text)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
if __name__ == "__main__":
|
|
331
|
+
# Test the BraveNews search
|
|
332
|
+
print("Testing BraveNews search...")
|
|
333
|
+
|
|
334
|
+
searcher = BraveNews(timeout=15)
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
# Test basic search
|
|
338
|
+
results = searcher.run("technology", max_results=5)
|
|
339
|
+
|
|
340
|
+
print(f"\nFound {len(results)} news results:\n")
|
|
341
|
+
for i, article in enumerate(results, 1):
|
|
342
|
+
print(f"{i}. {article.title}")
|
|
343
|
+
print(f" URL: {article.url}")
|
|
344
|
+
print(f" Source: {article.source}")
|
|
345
|
+
print(f" Date: {article.date}")
|
|
346
|
+
print(f" Description: {article.body[:100]}..." if article.body else "")
|
|
347
|
+
print(f" Image: {article.image[:80]}..." if article.image else "")
|
|
348
|
+
print()
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
print(f"Error during search: {e}")
|
|
352
|
+
import traceback
|
|
353
|
+
traceback.print_exc()
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Brave search suggestions/autocomplete implementation.
|
|
2
|
+
|
|
3
|
+
This module provides access to Brave Search's autocomplete/suggestions API
|
|
4
|
+
that returns search suggestions as users type.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from webscout.search.engines.brave.suggestions import BraveSuggestions
|
|
8
|
+
>>> suggester = BraveSuggestions()
|
|
9
|
+
>>> suggestions = suggester.run("pyth")
|
|
10
|
+
>>> for suggestion in suggestions:
|
|
11
|
+
... print(suggestion.query)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .base import BraveBase
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SuggestionResult:
|
|
25
|
+
"""A single search suggestion result.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
query: The suggested search query.
|
|
29
|
+
is_entity: Whether this suggestion represents a known entity.
|
|
30
|
+
name: Display name for entities.
|
|
31
|
+
desc: Description for entities.
|
|
32
|
+
category: Category of the entity (e.g., 'company', 'application').
|
|
33
|
+
image: URL to entity image/logo if available.
|
|
34
|
+
is_logo: Whether the image is a logo.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
query: str = ""
|
|
38
|
+
is_entity: bool = False
|
|
39
|
+
name: str = ""
|
|
40
|
+
desc: str = ""
|
|
41
|
+
category: str = ""
|
|
42
|
+
image: str = ""
|
|
43
|
+
is_logo: bool = False
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, Any]:
|
|
46
|
+
"""Convert to dictionary.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary representation of the suggestion.
|
|
50
|
+
"""
|
|
51
|
+
return {
|
|
52
|
+
"query": self.query,
|
|
53
|
+
"is_entity": self.is_entity,
|
|
54
|
+
"name": self.name,
|
|
55
|
+
"desc": self.desc,
|
|
56
|
+
"category": self.category,
|
|
57
|
+
"image": self.image,
|
|
58
|
+
"is_logo": self.is_logo,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class SuggestionsResponse:
|
|
64
|
+
"""Response containing search suggestions.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
query: The original query that was submitted.
|
|
68
|
+
suggestions: List of suggestion results.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
query: str = ""
|
|
72
|
+
suggestions: list[SuggestionResult] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
|
+
"""Convert to dictionary.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary representation of the response.
|
|
79
|
+
"""
|
|
80
|
+
return {
|
|
81
|
+
"query": self.query,
|
|
82
|
+
"suggestions": [s.to_dict() for s in self.suggestions],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BraveSuggestions(BraveBase):
|
|
87
|
+
"""Brave search suggestions/autocomplete engine.
|
|
88
|
+
|
|
89
|
+
Fetches search suggestions from Brave's autocomplete API endpoint.
|
|
90
|
+
Supports both simple suggestions and rich entity suggestions with
|
|
91
|
+
additional metadata like descriptions, categories, and images.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
name: Engine identifier name.
|
|
95
|
+
provider: Provider identifier.
|
|
96
|
+
category: Search category type.
|
|
97
|
+
api_url: URL for the suggestions API endpoint.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
name = "brave_suggestions"
|
|
101
|
+
provider = "brave"
|
|
102
|
+
category = "suggestions"
|
|
103
|
+
|
|
104
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
105
|
+
"""Initialize Brave suggestions client.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
*args: Positional arguments passed to BraveBase.
|
|
109
|
+
**kwargs: Keyword arguments passed to BraveBase.
|
|
110
|
+
"""
|
|
111
|
+
super().__init__(*args, **kwargs)
|
|
112
|
+
self.api_url = f"{self.base_url}/api/suggest"
|
|
113
|
+
|
|
114
|
+
def run(
|
|
115
|
+
self,
|
|
116
|
+
query: str,
|
|
117
|
+
rich: bool = True,
|
|
118
|
+
country: str | None = None,
|
|
119
|
+
source: str = "web",
|
|
120
|
+
max_results: int | None = None,
|
|
121
|
+
) -> list[SuggestionResult]:
|
|
122
|
+
"""Get search suggestions for a query.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
query: The partial search query to get suggestions for.
|
|
126
|
+
rich: Whether to include rich entity information (default True).
|
|
127
|
+
country: Country code for localized suggestions (e.g., 'us', 'in').
|
|
128
|
+
source: Search source type (default 'web').
|
|
129
|
+
max_results: Maximum number of suggestions to return.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of SuggestionResult objects containing suggestions.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If no query is provided.
|
|
136
|
+
Exception: If the API request fails.
|
|
137
|
+
"""
|
|
138
|
+
if not query:
|
|
139
|
+
raise ValueError("Query is mandatory")
|
|
140
|
+
|
|
141
|
+
params: dict[str, str] = {
|
|
142
|
+
"q": query,
|
|
143
|
+
"source": source,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if rich:
|
|
147
|
+
params["rich"] = "true"
|
|
148
|
+
|
|
149
|
+
if country:
|
|
150
|
+
params["country"] = country
|
|
151
|
+
|
|
152
|
+
response_data = self._fetch_suggestions(params)
|
|
153
|
+
suggestions = self._parse_response(response_data)
|
|
154
|
+
|
|
155
|
+
if max_results is not None:
|
|
156
|
+
suggestions = suggestions[:max_results]
|
|
157
|
+
|
|
158
|
+
return suggestions
|
|
159
|
+
|
|
160
|
+
def suggest(
|
|
161
|
+
self,
|
|
162
|
+
query: str,
|
|
163
|
+
rich: bool = True,
|
|
164
|
+
country: str | None = None,
|
|
165
|
+
) -> SuggestionsResponse:
|
|
166
|
+
"""Get search suggestions with full response object.
|
|
167
|
+
|
|
168
|
+
This method returns a SuggestionsResponse object containing
|
|
169
|
+
both the original query and the list of suggestions.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
query: The partial search query to get suggestions for.
|
|
173
|
+
rich: Whether to include rich entity information.
|
|
174
|
+
country: Country code for localized suggestions.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
SuggestionsResponse object with query and suggestions.
|
|
178
|
+
"""
|
|
179
|
+
suggestions = self.run(query, rich=rich, country=country)
|
|
180
|
+
return SuggestionsResponse(query=query, suggestions=suggestions)
|
|
181
|
+
|
|
182
|
+
def _fetch_suggestions(self, params: dict[str, str]) -> list[Any]:
|
|
183
|
+
"""Fetch suggestions from the Brave API.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
params: Query parameters for the API request.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Raw JSON response as a list.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
Exception: If the request fails.
|
|
193
|
+
"""
|
|
194
|
+
headers = dict(self.session.headers) if getattr(self, "session", None) else {}
|
|
195
|
+
headers.update({
|
|
196
|
+
"Referer": "https://search.brave.com/",
|
|
197
|
+
"Accept": "*/*",
|
|
198
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
resp = self.session.get(
|
|
203
|
+
self.api_url,
|
|
204
|
+
params=params,
|
|
205
|
+
headers=headers,
|
|
206
|
+
timeout=self.timeout,
|
|
207
|
+
)
|
|
208
|
+
resp.raise_for_status()
|
|
209
|
+
return resp.json()
|
|
210
|
+
except json.JSONDecodeError as e:
|
|
211
|
+
raise Exception(f"Failed to parse suggestions response: {e}") from e
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise Exception(f"Failed to fetch suggestions: {e}") from e
|
|
214
|
+
|
|
215
|
+
def _parse_response(self, data: list[Any]) -> list[SuggestionResult]:
|
|
216
|
+
"""Parse the API response into SuggestionResult objects.
|
|
217
|
+
|
|
218
|
+
The Brave suggestions API returns data in the format:
|
|
219
|
+
["query", [suggestion_objects]]
|
|
220
|
+
|
|
221
|
+
Each suggestion object can be either:
|
|
222
|
+
- Simple: {"is_entity": false, "q": "query text"}
|
|
223
|
+
- Entity: {"is_entity": true, "q": "query", "name": "...", "desc": "...",
|
|
224
|
+
"category": "...", "img": "...", "logo": true}
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
data: Raw JSON response from the API.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of SuggestionResult objects.
|
|
231
|
+
"""
|
|
232
|
+
results: list[SuggestionResult] = []
|
|
233
|
+
|
|
234
|
+
if not data or len(data) < 2:
|
|
235
|
+
return results
|
|
236
|
+
|
|
237
|
+
# Second element contains the suggestions list
|
|
238
|
+
suggestions_list = data[1] if len(data) > 1 else []
|
|
239
|
+
|
|
240
|
+
for item in suggestions_list:
|
|
241
|
+
if not isinstance(item, dict):
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
query_text = item.get("q", "")
|
|
245
|
+
is_entity = item.get("is_entity", False)
|
|
246
|
+
|
|
247
|
+
result = SuggestionResult(
|
|
248
|
+
query=query_text,
|
|
249
|
+
is_entity=is_entity,
|
|
250
|
+
name=item.get("name", ""),
|
|
251
|
+
desc=item.get("desc", ""),
|
|
252
|
+
category=item.get("category", ""),
|
|
253
|
+
image=item.get("img", ""),
|
|
254
|
+
is_logo=item.get("logo", False),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
results.append(result)
|
|
258
|
+
|
|
259
|
+
return results
|
|
260
|
+
|
|
261
|
+
def get_simple_suggestions(self, query: str, max_results: int = 10) -> list[str]:
|
|
262
|
+
"""Get a simple list of suggestion strings.
|
|
263
|
+
|
|
264
|
+
This is a convenience method that returns just the query strings
|
|
265
|
+
without the full SuggestionResult objects.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
query: The partial search query to get suggestions for.
|
|
269
|
+
max_results: Maximum number of suggestions to return.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of suggestion query strings.
|
|
273
|
+
"""
|
|
274
|
+
suggestions = self.run(query, rich=False, max_results=max_results)
|
|
275
|
+
return [s.query for s in suggestions if s.query]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
# Test the BraveSuggestions
|
|
280
|
+
print("Testing BraveSuggestions...")
|
|
281
|
+
|
|
282
|
+
suggester = BraveSuggestions(timeout=10)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
# Test basic suggestions
|
|
286
|
+
print("\n--- Testing with 'pyth' ---")
|
|
287
|
+
results = suggester.run("pyth")
|
|
288
|
+
|
|
289
|
+
print(f"Found {len(results)} suggestions:\n")
|
|
290
|
+
for i, suggestion in enumerate(results, 1):
|
|
291
|
+
if suggestion.is_entity:
|
|
292
|
+
print(f"{i}. [ENTITY] {suggestion.query}")
|
|
293
|
+
print(f" Name: {suggestion.name}")
|
|
294
|
+
print(f" Description: {suggestion.desc}")
|
|
295
|
+
print(f" Category: {suggestion.category}")
|
|
296
|
+
if suggestion.image:
|
|
297
|
+
print(f" Image: {suggestion.image[:60]}...")
|
|
298
|
+
else:
|
|
299
|
+
print(f"{i}. {suggestion.query}")
|
|
300
|
+
print()
|
|
301
|
+
|
|
302
|
+
# Test simple suggestions
|
|
303
|
+
print("\n--- Testing simple suggestions with 'java' ---")
|
|
304
|
+
simple = suggester.get_simple_suggestions("java", max_results=5)
|
|
305
|
+
print("Simple suggestions:")
|
|
306
|
+
for s in simple:
|
|
307
|
+
print(f" - {s}")
|
|
308
|
+
|
|
309
|
+
# Test full response object
|
|
310
|
+
print("\n--- Testing full response with 'react' ---")
|
|
311
|
+
response = suggester.suggest("react")
|
|
312
|
+
print(f"Query: {response.query}")
|
|
313
|
+
print(f"Suggestions count: {len(response.suggestions)}")
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
print(f"Error during test: {e}")
|
|
317
|
+
import traceback
|
|
318
|
+
traceback.print_exc()
|