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

1from __future__ import annotations 

2 

3from pathlib import Path 

4from typing import Any, Dict, Literal, cast 

5 

6from playwright.async_api import BrowserContext, StorageState 

7 

8from .families import CamoufoxFamily, PatchrightFamily, PlaywrightFamily 

9from .families.base import BrowserFamily, DesiredConfig, PlaywrightEngine 

10 

11Engine = Literal["chromium", "firefox", "webkit", "camoufox", "patchright"] 

12 

13 

14class BrowserMaster: 

15 """ 

16 Family aggregator. Holds the currently selected backend and delegates launch/close. 

17 Always returns a Browser. No persistent context. 

18 """ 

19 

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 {} # через сеттер ниже 

30 

31 self._family: BrowserFamily | None = None # активное семейство 

32 

33 self._validate_compat() 

34 

35 # ─────────── свойства (сеттеры не запускают, только меняют «desired») ─────────── 

36 

37 @property 

38 def engine(self) -> Engine: 

39 return self._engine 

40 

41 @engine.setter 

42 def engine(self, value: Engine) -> None: 

43 self._engine = value 

44 self._validate_compat() 

45 

46 @property 

47 def stealth(self) -> bool: 

48 return self._stealth_flag 

49 

50 @stealth.setter 

51 def stealth(self, value: bool) -> None: 

52 self._stealth_flag = bool(value) 

53 self._validate_compat() 

54 

55 @property 

56 def launch_opts(self) -> Dict[str, Any]: 

57 return self._launch_opts 

58 

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 

63 

64 # ─────────────────────────── публичные методы ─────────────────────────── 

65 

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 

73 

74 eng: PlaywrightEngine | None = ( 

75 cast(PlaywrightEngine, self._engine) if fam.name == "playwright" else None 

76 ) 

77 

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) 

85 

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 

95 

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) 

104 

105 # ─────────────────────────── внутреннее ─────────────────────────── 

106 

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() 

118 

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.")