Coverage for human_requests/browsers/browser_master.py: 85%
66 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 21:41 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 21:41 +0000
1from __future__ import annotations
3from pathlib import Path
4from typing import Any, Dict, Literal, cast
6from playwright.async_api import BrowserContext, StorageState
8from .families import CamoufoxFamily, PatchrightFamily, PlaywrightFamily
9from .families.base import BrowserFamily, DesiredConfig, PlaywrightEngine
11Engine = Literal["chromium", "firefox", "webkit", "camoufox", "patchright"]
14class BrowserMaster:
15 """
16 Family aggregator. Holds the currently selected backend and delegates launch/close.
17 Always returns a Browser. No persistent context.
18 """
20 def __init__(
21 self,
22 *,
23 engine: Engine = "chromium",
24 stealth: bool = False,
25 launch_opts: Dict[str, Any] | None = None,
26 ) -> None:
27 self._engine: Engine = engine
28 self._stealth_flag: bool = stealth
29 self.launch_opts = launch_opts or {} # через сеттер ниже
31 self._family: BrowserFamily | None = None # активное семейство
33 self._validate_compat()
35 # ─────────── свойства (сеттеры не запускают, только меняют «desired») ───────────
37 @property
38 def engine(self) -> Engine:
39 return self._engine
41 @engine.setter
42 def engine(self, value: Engine) -> None:
43 self._engine = value
44 self._validate_compat()
46 @property
47 def stealth(self) -> bool:
48 return self._stealth_flag
50 @stealth.setter
51 def stealth(self, value: bool) -> None:
52 self._stealth_flag = bool(value)
53 self._validate_compat()
55 @property
56 def launch_opts(self) -> Dict[str, Any]:
57 return self._launch_opts
59 @launch_opts.setter
60 def launch_opts(self, value: Dict[str, Any] | None) -> None:
61 opts = dict(value or {})
62 self._launch_opts = opts
64 # ─────────────────────────── публичные методы ───────────────────────────
66 async def start(self) -> None:
67 """Idempotent launch of the current family. Switches family if necessary."""
68 fam = self._select_family(self._engine)
69 if self._family is None or (self._family.name != fam.name):
70 # переключаемся на другое семейство — закрываем прежнее
71 await self.close(camoufox=True, playwright=True)
72 self._family = fam
74 eng: PlaywrightEngine | None = (
75 cast(PlaywrightEngine, self._engine) if fam.name == "playwright" else None
76 )
78 cfg = DesiredConfig(
79 family=fam.name,
80 engine=eng,
81 stealth=self._stealth_flag,
82 launch_opts=self._launch_opts,
83 )
84 await self._family.start(cfg)
86 async def close(self, *, camoufox: bool = True, playwright: bool = True) -> None:
87 """Selective shutdown: camoufox → CamoufoxFamily; playwright → Playwright/Patchright."""
88 if self._family is None:
89 return
90 if (self._family.name == "camoufox" and camoufox) or (
91 self._family.name in ("playwright", "patchright") and playwright
92 ):
93 await self._family.close()
94 self._family = None
96 async def new_context(
97 self,
98 *,
99 storage_state: StorageState | str | Path | None = None,
100 ) -> BrowserContext:
101 await self.start()
102 assert self._family is not None
103 return await self._family.new_context(storage_state=storage_state)
105 # ─────────────────────────── внутреннее ───────────────────────────
107 def _select_family(self, engine: Engine) -> BrowserFamily:
108 if engine == "camoufox":
109 if self._stealth_flag:
110 raise RuntimeError("stealth несовместим с engine='camoufox'.")
111 return CamoufoxFamily()
112 if engine == "patchright":
113 if self._stealth_flag:
114 raise RuntimeError("stealth несовместим с engine='patchright'.")
115 return PatchrightFamily()
116 # обычный Playwright
117 return PlaywrightFamily()
119 def _validate_compat(self) -> None:
120 if self._engine in ("camoufox", "patchright") and self._stealth_flag:
121 raise RuntimeError(f"stealth несовместим с engine='{self._engine}'. Отключите stealth.")