Skip to content

Commit 2e5152f

Browse files
authored
v0.3.12
2 parents f6862f8 + a7b55d4 commit 2e5152f

14 files changed

Lines changed: 97 additions & 65 deletions

File tree

.github/workflows/tests.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858
- name: Install all browsers dependencies
5959
run: |
6060
python3 -m pip install --upgrade pip
61-
python3 -m pip install playwright>=1.55.0 patchright>=1.55.0 camoufox
61+
python3 -m pip install playwright==1.56.0 patchright==1.56.0 camoufox>=0.4.11
6262
6363
- name: Get Playwright version
6464
id: playwright-version
@@ -83,7 +83,11 @@ jobs:
8383
- name: Install Playwright browsers
8484
run: |
8585
echo "Cache hit: ${{ steps.playwright-cache.outputs.cache-hit }}"
86-
python3 -m playwright install chromium
86+
if [ "${{ steps.playwright-cache.outputs.cache-hit }}" != "true" ]; then
87+
python3 -m playwright install chromium
88+
else
89+
echo "Skipping install - using cached Playwright browsers"
90+
fi
8791
python3 -m playwright install-deps chromium firefox
8892
8993
- name: Get Camoufox version
@@ -108,7 +112,11 @@ jobs:
108112
- name: Install Camoufox browser
109113
run: |
110114
echo "Cache hit: ${{ steps.camoufox-cache.outputs.cache-hit }}"
111-
python3 -m camoufox fetch --browserforge
115+
if [ "${{ steps.camoufox-cache.outputs.cache-hit }}" != "true" ]; then
116+
python3 -m camoufox fetch --browserforge
117+
else
118+
echo "Skipping fetch - using cached Camoufox browser"
119+
fi
112120
113121
# Cache tox environments
114122
- name: Cache tox environments

docs/fetching/dynamic.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Scrapling provides many options with this fetcher and its session classes. To ma
9393
| stealth | Enables stealth mode; you should always check the documentation to see what the stealth mode does currently. | ✔️ |
9494
| real_chrome | If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch and use an instance of your browser. | ✔️ |
9595
| locale | Set the locale for the browser if wanted. The default value is `en-US`. | ✔️ |
96+
| timezone_id | Set the timezone for the browser if wanted. | ✔️ |
9697
| cdp_url | Instead of launching a new browser instance, connect to this CDP URL to control real browsers through CDP. | ✔️ |
9798
| user_data_dir | Path to a User Data Directory, which stores browser session data like cookies and local storage. The default is to create a temporary directory. **Only Works with sessions** | ✔️ |
9899
| extra_flags | A list of additional browser flags to pass to the browser on launch. | ✔️ |

pyproject.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "scrapling"
77
# Static version instead of a dynamic version so we can get better layer caching while building docker, check the docker file to understand
8-
version = "0.3.11"
8+
version = "0.3.12"
99
description = "Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy and effortless as it should be!"
1010
readme = {file = "docs/README.md", content-type = "text/markdown"}
1111
license = {file = "LICENSE"}
@@ -59,22 +59,22 @@ classifiers = [
5959
dependencies = [
6060
"lxml>=6.0.2",
6161
"cssselect>=1.3.0",
62-
"orjson>=3.11.4",
62+
"orjson>=3.11.5",
6363
"tldextract>=5.3.0",
6464
]
6565

6666
[project.optional-dependencies]
6767
fetchers = [
6868
"click>=8.3.0",
69-
"curl_cffi>=0.13.0",
70-
"playwright>=1.56.0",
71-
"patchright>=1.56.0",
69+
"curl_cffi>=0.14.0",
70+
"playwright==1.56.0",
71+
"patchright==1.56.0",
7272
"camoufox>=0.4.11",
7373
"geoip2>=5.2.0",
7474
"msgspec>=0.20.0",
7575
]
7676
ai = [
77-
"mcp>=1.23.0",
77+
"mcp>=1.24.0",
7878
"markdownify>=1.2.0",
7979
"scrapling[fetchers]",
8080
]

scrapling/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__author__ = "Karim Shoair (karim.shoair@pm.me)"
2-
__version__ = "0.3.11"
2+
__version__ = "0.3.12"
33
__copyright__ = "Copyright (c) 2024 Karim Shoair"
44

55
from typing import Any, TYPE_CHECKING

scrapling/cli.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
from subprocess import check_output
33
from sys import executable as python_executable
44

5-
from curl_cffi.requests import impersonate
6-
75
from scrapling.core.utils import log
86
from scrapling.engines.toolbelt.custom import Response
97
from scrapling.core.utils._shell import _CookieParser, _ParseHeaders

scrapling/engines/_browsers/_base.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def __init__(self, max_pages: int = 1):
4040
self.context: BrowserContext | Any = None
4141
self._closed = False
4242

43-
def __create__(self):
43+
def start(self):
4444
pass
4545

4646
def close(self): # pragma: no cover
@@ -59,7 +59,7 @@ def close(self): # pragma: no cover
5959
self._closed = True
6060

6161
def __enter__(self):
62-
self.__create__()
62+
self.start()
6363
return self
6464

6565
def __exit__(self, exc_type, exc_val, exc_tb):
@@ -145,7 +145,7 @@ def __init__(self, max_pages: int = 1):
145145
self._closed = False
146146
self._lock = Lock()
147147

148-
async def __create__(self):
148+
async def start(self):
149149
pass
150150

151151
async def close(self):
@@ -164,7 +164,7 @@ async def close(self):
164164
self._closed = True
165165

166166
async def __aenter__(self):
167-
await self.__create__()
167+
await self.start()
168168
return self
169169

170170
async def __aexit__(self, exc_type, exc_val, exc_tb):
@@ -281,6 +281,7 @@ def __validate__(self, **params):
281281
self._wait_selector_state = config.wait_selector_state
282282
self._extra_flags = config.extra_flags
283283
self._selector_config = config.selector_config
284+
self._timezone_id = config.timezone_id
284285
self._additional_args = config.additional_args
285286
self._page_action = config.page_action
286287
self._user_data_dir = config.user_data_dir
@@ -304,6 +305,7 @@ def __initiate_browser_options__(self):
304305
self._stealth,
305306
self._hide_canvas,
306307
self._disable_webgl,
308+
self._timezone_id,
307309
tuple(self._extra_flags) if self._extra_flags else tuple(),
308310
)
309311
)

scrapling/engines/_browsers/_camoufox.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,19 @@ def __init__(self, **kwargs: Unpack[CamoufoxSession]):
102102
self.__validate__(**kwargs)
103103
super().__init__(max_pages=self._max_pages)
104104

105-
def __create__(self):
105+
def start(self):
106106
"""Create a browser for this instance and context."""
107-
self.playwright = sync_playwright().start()
108-
self.context = self.playwright.firefox.launch_persistent_context(**self.launch_options)
107+
if not self.playwright:
108+
self.playwright = sync_playwright().start()
109+
self.context = self.playwright.firefox.launch_persistent_context(**self.launch_options)
109110

110-
if self._init_script: # pragma: no cover
111-
self.context.add_init_script(path=self._init_script)
111+
if self._init_script: # pragma: no cover
112+
self.context.add_init_script(path=self._init_script)
112113

113-
if self._cookies: # pragma: no cover
114-
self.context.add_cookies(self._cookies)
114+
if self._cookies: # pragma: no cover
115+
self.context.add_cookies(self._cookies)
116+
else:
117+
raise RuntimeError("Session has been already started")
115118

116119
def _cloudflare_solver(self, page: Page) -> None: # pragma: no cover
117120
"""Solve the cloudflare challenge displayed on the playwright page passed
@@ -299,18 +302,21 @@ def __init__(self, **kwargs: Unpack[CamoufoxSession]):
299302
self.__validate__(**kwargs)
300303
super().__init__(max_pages=self._max_pages)
301304

302-
async def __create__(self):
305+
async def start(self):
303306
"""Create a browser for this instance and context."""
304-
self.playwright: AsyncPlaywright = await async_playwright().start()
305-
self.context: AsyncBrowserContext = await self.playwright.firefox.launch_persistent_context(
306-
**self.launch_options
307-
)
307+
if not self.playwright:
308+
self.playwright: AsyncPlaywright = await async_playwright().start()
309+
self.context: AsyncBrowserContext = await self.playwright.firefox.launch_persistent_context(
310+
**self.launch_options
311+
)
308312

309-
if self._init_script: # pragma: no cover
310-
await self.context.add_init_script(path=self._init_script)
313+
if self._init_script: # pragma: no cover
314+
await self.context.add_init_script(path=self._init_script)
311315

312-
if self._cookies:
313-
await self.context.add_cookies(self._cookies) # pyright: ignore [reportArgumentType]
316+
if self._cookies:
317+
await self.context.add_cookies(self._cookies) # pyright: ignore [reportArgumentType]
318+
else:
319+
raise RuntimeError("Session has been already started")
314320

315321
async def _cloudflare_solver(self, page: async_Page): # pragma: no cover
316322
"""Solve the cloudflare challenge displayed on the playwright page passed. The async version

scrapling/engines/_browsers/_config_tools.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def _launch_kwargs(
7070
stealth,
7171
hide_canvas,
7272
disable_webgl,
73+
timezone_id,
7374
extra_flags: Tuple,
7475
) -> Tuple:
7576
"""Creates the arguments we will use while launching playwright's browser"""
@@ -79,6 +80,7 @@ def _launch_kwargs(
7980

8081
launch_kwargs = {
8182
"locale": locale,
83+
"timezone_id": timezone_id or None,
8284
"headless": headless,
8385
"args": base_args,
8486
"color_scheme": "dark", # Bypasses the 'prefersLightColor' check in creepjs

scrapling/engines/_browsers/_controllers.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__(self, **kwargs: Unpack[PlaywrightSession]):
7777
:param wait_selector: Wait for a specific CSS selector to be in a specific state.
7878
:param init_script: An absolute path to a JavaScript file to be executed on page creation for all pages in this session.
7979
:param locale: Set the locale for the browser if wanted. The default value is `en-US`.
80+
:param timezone_id: Set the timezone for the browser if wanted.
8081
:param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`.
8182
:param stealth: Enables stealth mode, check the documentation to see what stealth mode does currently.
8283
:param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it.
@@ -95,24 +96,27 @@ def __init__(self, **kwargs: Unpack[PlaywrightSession]):
9596
self.__validate__(**kwargs)
9697
super().__init__(max_pages=self._max_pages)
9798

98-
def __create__(self):
99+
def start(self):
99100
"""Create a browser for this instance and context."""
100-
sync_context = sync_patchright if self._stealth else sync_playwright
101+
if not self.playwright:
102+
sync_context = sync_patchright if self._stealth else sync_playwright
101103

102-
self.playwright: Playwright = sync_context().start() # pyright: ignore [reportAttributeAccessIssue]
104+
self.playwright: Playwright = sync_context().start() # pyright: ignore [reportAttributeAccessIssue]
103105

104-
if self._cdp_url: # pragma: no cover
105-
self.context = self.playwright.chromium.connect_over_cdp(endpoint_url=self._cdp_url).new_context(
106-
**self.context_options
107-
)
108-
else:
109-
self.context = self.playwright.chromium.launch_persistent_context(**self.launch_options)
106+
if self._cdp_url: # pragma: no cover
107+
self.context = self.playwright.chromium.connect_over_cdp(endpoint_url=self._cdp_url).new_context(
108+
**self.context_options
109+
)
110+
else:
111+
self.context = self.playwright.chromium.launch_persistent_context(**self.launch_options)
110112

111-
if self._init_script: # pragma: no cover
112-
self.context.add_init_script(path=self._init_script)
113+
if self._init_script: # pragma: no cover
114+
self.context.add_init_script(path=self._init_script)
113115

114-
if self._cookies: # pragma: no cover
115-
self.context.add_cookies(self._cookies)
116+
if self._cookies: # pragma: no cover
117+
self.context.add_cookies(self._cookies)
118+
else:
119+
raise RuntimeError("Session has been already started")
116120

117121
def fetch(self, url: str, **kwargs: Unpack[PlaywrightFetchParams]) -> Response:
118122
"""Opens up the browser and do your request based on your chosen options.
@@ -209,6 +213,7 @@ def __init__(self, **kwargs: Unpack[PlaywrightSession]):
209213
:param wait_selector: Wait for a specific CSS selector to be in a specific state.
210214
:param init_script: An absolute path to a JavaScript file to be executed on page creation for all pages in this session.
211215
:param locale: Set the locale for the browser if wanted. The default value is `en-US`.
216+
:param timezone_id: Set the timezone for the browser if wanted.
212217
:param wait_selector_state: The state to wait for the selector given with `wait_selector`. The default state is `attached`.
213218
:param stealth: Enables stealth mode, check the documentation to see what stealth mode does currently.
214219
:param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it.
@@ -227,25 +232,28 @@ def __init__(self, **kwargs: Unpack[PlaywrightSession]):
227232
self.__validate__(**kwargs)
228233
super().__init__(max_pages=self._max_pages)
229234

230-
async def __create__(self):
235+
async def start(self):
231236
"""Create a browser for this instance and context."""
232-
async_context = async_patchright if self._stealth else async_playwright
237+
if not self.playwright:
238+
async_context = async_patchright if self._stealth else async_playwright
233239

234-
self.playwright: AsyncPlaywright = await async_context().start() # pyright: ignore [reportAttributeAccessIssue]
240+
self.playwright: AsyncPlaywright = await async_context().start() # pyright: ignore [reportAttributeAccessIssue]
235241

236-
if self._cdp_url:
237-
browser = await self.playwright.chromium.connect_over_cdp(endpoint_url=self._cdp_url)
238-
self.context: AsyncBrowserContext = await browser.new_context(**self.context_options)
239-
else:
240-
self.context: AsyncBrowserContext = await self.playwright.chromium.launch_persistent_context(
241-
**self.launch_options
242-
)
242+
if self._cdp_url:
243+
browser = await self.playwright.chromium.connect_over_cdp(endpoint_url=self._cdp_url)
244+
self.context: AsyncBrowserContext = await browser.new_context(**self.context_options)
245+
else:
246+
self.context: AsyncBrowserContext = await self.playwright.chromium.launch_persistent_context(
247+
**self.launch_options
248+
)
243249

244-
if self._init_script: # pragma: no cover
245-
await self.context.add_init_script(path=self._init_script)
250+
if self._init_script: # pragma: no cover
251+
await self.context.add_init_script(path=self._init_script)
246252

247-
if self._cookies:
248-
await self.context.add_cookies(self._cookies) # pyright: ignore
253+
if self._cookies:
254+
await self.context.add_cookies(self._cookies) # pyright: ignore
255+
else:
256+
raise RuntimeError("Session has been already started")
249257

250258
async def fetch(self, url: str, **kwargs: Unpack[PlaywrightFetchParams]) -> Response:
251259
"""Opens up the browser and do your request based on your chosen options.

scrapling/engines/_browsers/_validators.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class PlaywrightConfig(Struct, kw_only=True, frozen=False, weakref=True):
8787
load_dom: bool = True
8888
wait_selector_state: SelectorWaitStates = "attached"
8989
user_data_dir: str = ""
90+
timezone_id: str = ""
9091
extra_flags: Optional[List[str]] = None
9192
selector_config: Optional[Dict] = {}
9293
additional_args: Optional[Dict] = {}

0 commit comments

Comments
 (0)