scrapely-python 0.1.42__tar.gz → 0.1.43__tar.gz
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.
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/CHANGELOG.md +11 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/PKG-INFO +1 -1
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/pyproject.toml +1 -1
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/__init__.py +182 -84
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/charging_client.py +21 -5
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/patch_browser_pool.py +174 -72
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely_python.egg-info/PKG-INFO +1 -1
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/LICENSE +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/MANIFEST.in +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/NOTICE +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/README.md +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/setup.cfg +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/actor.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/cdp_retry.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/charging.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_capture.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_compat.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_detect.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_headers.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_options.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_policy.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_profiles.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_response.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_stats.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/_transport.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/http/profiles.json +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/proxy_configuration.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/_actor_runner.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/_async_thread.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/_logging_config.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/_serialization.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/_warnings.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/extensions/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/extensions/_graceful_stop.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/extensions/_httpcache.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/middlewares/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/middlewares/apify_proxy.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/pipelines/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/pipelines/actor_dataset_push.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/requests.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/scheduler.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/utils.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/solver.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/api_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely_python.egg-info/requires.txt +0 -0
- {scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely_python.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.43] - 2026-11-05
|
|
8
|
+
|
|
9
|
+
- **Python actor processes start without loading the SDK.** The base images import `scrapely.patch_browser_pool` (from `sitecustomize`) in every Python process. Until now that loaded the whole SDK at each interpreter start: `scrapely.actor`, charging, the `scrapely_client` models and Playwright. On `actor-python:3.11-0.1.42`, `python -c pass` took 0.89-0.95 s of CPU, which is 4.2 s of wall time at a 0.25-core quota. It now takes 0.016 s. Each import below gets the same start-up cut:
|
|
10
|
+
- `import scrapely` is lazy (PEP 562). Every public name and submodule attribute resolves on first use, so every `from scrapely import X` and `scrapely.X` that worked before still works.
|
|
11
|
+
- `scrapely_client` loads only when an API client is created (`Actor.apify_client`, `create_charging_client`). `USES_PUSH_DATA_INTERCEPTION` and `push_data_charging_context` are still the client's own objects.
|
|
12
|
+
- **Browser patches are applied through an import hook.** `scrapely.patch_browser_pool` imports no browser library any more; it imports builtin modules only. Each patch now runs the moment its library finishes loading, before the import that loaded it returns. That covers the Crawlee BrowserPool / PlaywrightBrowserPlugin / PlaywrightCrawler, raw Playwright (`async_playwright` and `sync_playwright`, each when its module loads), Camoufox `AsyncNewBrowser` and pyppeteer `launch`. The patches behave exactly as before. The hook also:
|
|
13
|
+
- survives `importlib.util.find_spec()` availability checks and a failed first import;
|
|
14
|
+
- logs a warning when a patch fails instead of breaking the actor's import.
|
|
15
|
+
- **Start-up measured on staging** (fresh builds of the same commit on the same base, old SDK vs 0.1.43; "boot" = container started to the SDK's "Initializing API storage" line; paired median): web-scraper -0.74 s, capterra-scraper (raw Playwright over the remote browser) -0.89 s, contact-info-scraper (512 MB) -0.51 s. All 88 scenario runs passed on both sides. Locally at 1 CPU, a full `from apify import Actor` + `Actor.init` + `push_data` + exit went from 1.19-1.36 s to 0.92-1.02 s of CPU.
|
|
16
|
+
- Base images (released with this version): `sitecustomize` no longer imports `pathlib` (~70 ms per start), and the `apify` shim resolves names on first use instead of `from scrapely import *`.
|
|
17
|
+
|
|
7
18
|
## [0.1.42] - 2026-11-02
|
|
8
19
|
|
|
9
20
|
- **Sessions that keep their identity: `http.session(keep_identity=True)`.** A pinned `scrapely.http` session used to retire its identity on the first block (a 403, a challenge page, a network error). The session then moved to a new exit, a new profile and an empty cookie jar. That breaks every challenge solve: the challenge page, the solver's script and submission requests and the page fetched again must all share one identity. With `keep_identity=True`:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scrapely-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.43"
|
|
8
8
|
description = "Scrapely Python SDK - A toolkit for building Actors on the Scrapely platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT AND Apache-2.0"
|
|
@@ -10,95 +10,114 @@ Usage:
|
|
|
10
10
|
await Actor.push_data({'url': actor_input['url']})
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# ``typing`` is not imported at run time (see below): this package loads at
|
|
16
|
+
# every interpreter start in actor images.
|
|
17
|
+
TYPE_CHECKING = False
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from typing import Any
|
|
14
20
|
|
|
15
21
|
# Side-effect: patch browser launches (Crawlee BrowserPool, PlaywrightCrawler,
|
|
16
22
|
# raw Playwright, Camoufox, pyppeteer) to use the remote CDP browser (CDP_URL).
|
|
17
23
|
# Must run on package import so `from scrapely import Actor` enables the patch
|
|
18
|
-
# before any browser is launched (Node SDK parity). It
|
|
19
|
-
#
|
|
24
|
+
# before any browser is launched (Node SDK parity). It imports none of those
|
|
25
|
+
# libraries: each patch is applied the moment its library finishes loading.
|
|
20
26
|
from .patch_browser_pool import set_cdp_url # noqa: F401
|
|
21
27
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
from
|
|
28
|
-
from .configuration import Configuration
|
|
29
|
-
from .charging_client import (
|
|
30
|
-
USES_PUSH_DATA_INTERCEPTION,
|
|
31
|
-
create_charging_client,
|
|
32
|
-
push_data_charging_context,
|
|
33
|
-
)
|
|
34
|
-
from .proxy_configuration import ProxyConfiguration
|
|
35
|
-
from .browser_configuration import BrowserConfiguration
|
|
36
|
-
from .charging import (
|
|
37
|
-
ChargingManager,
|
|
38
|
-
ChargeUncertainError,
|
|
39
|
-
ChargeRejectedError,
|
|
40
|
-
has_default_dataset_item_event_in_pricing,
|
|
41
|
-
is_default_dataset_item_event,
|
|
42
|
-
merge_charge_results,
|
|
43
|
-
push_data_and_charge,
|
|
44
|
-
resolve_default_dataset_item_event_name,
|
|
45
|
-
APIFY_DEFAULT_DATASET_ITEM_EVENT,
|
|
46
|
-
SCRAPELY_DEFAULT_DATASET_ITEM_EVENT,
|
|
47
|
-
DEFAULT_DATASET_ITEM_EVENT,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
from .types import (
|
|
51
|
-
ActorInput,
|
|
52
|
-
ActorPermissionLevel,
|
|
53
|
-
ActorRun,
|
|
54
|
-
ApifyEnv,
|
|
55
|
-
AbortOptions,
|
|
56
|
-
CallOptions,
|
|
57
|
-
ExitOptions,
|
|
58
|
-
InitOptions,
|
|
59
|
-
MetamorphOptions,
|
|
60
|
-
OpenStorageOptions,
|
|
61
|
-
ProxyInfo,
|
|
62
|
-
RebootOptions,
|
|
63
|
-
RecordOptions,
|
|
64
|
-
SetStatusMessageOptions,
|
|
65
|
-
StartOptions,
|
|
66
|
-
WebhookOptions,
|
|
67
|
-
)
|
|
28
|
+
# Nothing else is imported here. The base images' sitecustomize imports
|
|
29
|
+
# ``scrapely.patch_browser_pool`` (and so this package) in EVERY Python process
|
|
30
|
+
# of an actor image; loading the whole SDK there cost ~0.75 s per interpreter
|
|
31
|
+
# start at 1 CPU (5.7 s at 0.25 CPU). Every public name is resolved on first
|
|
32
|
+
# attribute access by ``__getattr__`` below and cached in the module namespace,
|
|
33
|
+
# so every ``from scrapely import X`` that worked before still works.
|
|
68
34
|
|
|
69
|
-
|
|
70
|
-
Webhook,
|
|
71
|
-
WebhookEventType,
|
|
72
|
-
)
|
|
73
|
-
from .pricing_types import (
|
|
74
|
-
PricingModel,
|
|
75
|
-
FreeActorPricingInfo,
|
|
76
|
-
FlatPricePerMonthActorPricingInfo,
|
|
77
|
-
PricePerDatasetItemActorPricingInfo,
|
|
78
|
-
PayPerEventActorPricingInfo,
|
|
79
|
-
PricingPerEvent,
|
|
80
|
-
ActorPricingInfo,
|
|
81
|
-
ActorChargeEvent,
|
|
82
|
-
ChargeResult,
|
|
83
|
-
ActorPricingInfoResult,
|
|
84
|
-
)
|
|
85
|
-
from .errors import (
|
|
86
|
-
ScrapelyClientError,
|
|
87
|
-
ScrapelyApiError,
|
|
88
|
-
InvalidResponseBodyError,
|
|
89
|
-
InvalidRequestError,
|
|
90
|
-
UnauthorizedError,
|
|
91
|
-
ForbiddenError,
|
|
92
|
-
NotFoundError,
|
|
93
|
-
ConflictError,
|
|
94
|
-
RateLimitError,
|
|
95
|
-
ServerError,
|
|
96
|
-
raise_for_status,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Public name -> (module, attribute) for names that need Crawlee. Loaded on
|
|
100
|
-
# first access and cached in the module namespace.
|
|
35
|
+
# Public name -> (module, attribute). Loaded on first access.
|
|
101
36
|
_LAZY_ATTRS = {
|
|
37
|
+
'Actor': ('scrapely.actor', 'Actor'),
|
|
38
|
+
'Configuration': ('scrapely.configuration', 'Configuration'),
|
|
39
|
+
'ProxyConfiguration': ('scrapely.proxy_configuration', 'ProxyConfiguration'),
|
|
40
|
+
'BrowserConfiguration': ('scrapely.browser_configuration', 'BrowserConfiguration'),
|
|
41
|
+
# Charging client
|
|
42
|
+
**{
|
|
43
|
+
name: ('scrapely.charging_client', name)
|
|
44
|
+
for name in ('USES_PUSH_DATA_INTERCEPTION', 'create_charging_client', 'push_data_charging_context')
|
|
45
|
+
},
|
|
46
|
+
# Charging
|
|
47
|
+
**{
|
|
48
|
+
name: ('scrapely.charging', name)
|
|
49
|
+
for name in (
|
|
50
|
+
'ChargingManager',
|
|
51
|
+
'ChargeUncertainError',
|
|
52
|
+
'ChargeRejectedError',
|
|
53
|
+
'has_default_dataset_item_event_in_pricing',
|
|
54
|
+
'is_default_dataset_item_event',
|
|
55
|
+
'merge_charge_results',
|
|
56
|
+
'push_data_and_charge',
|
|
57
|
+
'resolve_default_dataset_item_event_name',
|
|
58
|
+
'APIFY_DEFAULT_DATASET_ITEM_EVENT',
|
|
59
|
+
'SCRAPELY_DEFAULT_DATASET_ITEM_EVENT',
|
|
60
|
+
'DEFAULT_DATASET_ITEM_EVENT',
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
# Types
|
|
64
|
+
**{
|
|
65
|
+
name: ('scrapely.types', name)
|
|
66
|
+
for name in (
|
|
67
|
+
'ActorInput',
|
|
68
|
+
'ActorPermissionLevel',
|
|
69
|
+
'ActorRun',
|
|
70
|
+
'ApifyEnv',
|
|
71
|
+
'AbortOptions',
|
|
72
|
+
'CallOptions',
|
|
73
|
+
'ExitOptions',
|
|
74
|
+
'InitOptions',
|
|
75
|
+
'MetamorphOptions',
|
|
76
|
+
'OpenStorageOptions',
|
|
77
|
+
'ProxyInfo',
|
|
78
|
+
'RebootOptions',
|
|
79
|
+
'RecordOptions',
|
|
80
|
+
'SetStatusMessageOptions',
|
|
81
|
+
'StartOptions',
|
|
82
|
+
'WebhookOptions',
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
# Webhook
|
|
86
|
+
'Webhook': ('scrapely.webhook', 'Webhook'),
|
|
87
|
+
'WebhookEventType': ('scrapely.webhook', 'WebhookEventType'),
|
|
88
|
+
# Pricing
|
|
89
|
+
**{
|
|
90
|
+
name: ('scrapely.pricing_types', name)
|
|
91
|
+
for name in (
|
|
92
|
+
'PricingModel',
|
|
93
|
+
'FreeActorPricingInfo',
|
|
94
|
+
'FlatPricePerMonthActorPricingInfo',
|
|
95
|
+
'PricePerDatasetItemActorPricingInfo',
|
|
96
|
+
'PayPerEventActorPricingInfo',
|
|
97
|
+
'PricingPerEvent',
|
|
98
|
+
'ActorPricingInfo',
|
|
99
|
+
'ActorChargeEvent',
|
|
100
|
+
'ChargeResult',
|
|
101
|
+
'ActorPricingInfoResult',
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
# Errors
|
|
105
|
+
**{
|
|
106
|
+
name: ('scrapely.errors', name)
|
|
107
|
+
for name in (
|
|
108
|
+
'ScrapelyClientError',
|
|
109
|
+
'ScrapelyApiError',
|
|
110
|
+
'InvalidResponseBodyError',
|
|
111
|
+
'InvalidRequestError',
|
|
112
|
+
'UnauthorizedError',
|
|
113
|
+
'ForbiddenError',
|
|
114
|
+
'NotFoundError',
|
|
115
|
+
'ConflictError',
|
|
116
|
+
'RateLimitError',
|
|
117
|
+
'ServerError',
|
|
118
|
+
'raise_for_status',
|
|
119
|
+
)
|
|
120
|
+
},
|
|
102
121
|
# Crawlee re-exports
|
|
103
122
|
'Request': ('crawlee', 'Request'),
|
|
104
123
|
'RequestLoader': ('crawlee.request_loaders', 'RequestLoader'),
|
|
@@ -150,15 +169,28 @@ _LAZY_ATTRS = {
|
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
# Submodules that used to be loaded (and so bound as attributes) by this
|
|
153
|
-
# package's eager imports; ``scrapely.events`` etc. keep
|
|
154
|
-
# explicit ``import scrapely
|
|
172
|
+
# package's eager imports; ``scrapely.actor``, ``scrapely.events`` etc. keep
|
|
173
|
+
# working without an explicit ``import scrapely.<name>``.
|
|
155
174
|
_LAZY_SUBMODULES = frozenset({
|
|
175
|
+
'_browser_locations',
|
|
176
|
+
'actor',
|
|
177
|
+
'browser_configuration',
|
|
178
|
+
'cdp_retry',
|
|
179
|
+
'charging',
|
|
180
|
+
'charging_client',
|
|
181
|
+
'configuration',
|
|
182
|
+
'crypto',
|
|
183
|
+
'errors',
|
|
156
184
|
'events',
|
|
157
185
|
'log',
|
|
186
|
+
'pricing_types',
|
|
187
|
+
'proxy_configuration',
|
|
158
188
|
'request_loaders',
|
|
159
189
|
'storage',
|
|
160
190
|
'storage_clients',
|
|
161
191
|
'storages',
|
|
192
|
+
'types',
|
|
193
|
+
'webhook',
|
|
162
194
|
})
|
|
163
195
|
|
|
164
196
|
|
|
@@ -193,6 +225,40 @@ if TYPE_CHECKING: # Static analysers and IDEs see the lazy names as plain impor
|
|
|
193
225
|
from crawlee.storage_clients.models import DatasetItemsListPage
|
|
194
226
|
from crawlee.storages import Dataset, KeyValueStore, RequestQueue
|
|
195
227
|
|
|
228
|
+
from .actor import Actor
|
|
229
|
+
from .browser_configuration import BrowserConfiguration
|
|
230
|
+
from .charging import (
|
|
231
|
+
APIFY_DEFAULT_DATASET_ITEM_EVENT,
|
|
232
|
+
DEFAULT_DATASET_ITEM_EVENT,
|
|
233
|
+
SCRAPELY_DEFAULT_DATASET_ITEM_EVENT,
|
|
234
|
+
ChargeRejectedError,
|
|
235
|
+
ChargeUncertainError,
|
|
236
|
+
ChargingManager,
|
|
237
|
+
has_default_dataset_item_event_in_pricing,
|
|
238
|
+
is_default_dataset_item_event,
|
|
239
|
+
merge_charge_results,
|
|
240
|
+
push_data_and_charge,
|
|
241
|
+
resolve_default_dataset_item_event_name,
|
|
242
|
+
)
|
|
243
|
+
from .charging_client import (
|
|
244
|
+
USES_PUSH_DATA_INTERCEPTION,
|
|
245
|
+
create_charging_client,
|
|
246
|
+
push_data_charging_context,
|
|
247
|
+
)
|
|
248
|
+
from .configuration import Configuration
|
|
249
|
+
from .errors import (
|
|
250
|
+
ConflictError,
|
|
251
|
+
ForbiddenError,
|
|
252
|
+
InvalidRequestError,
|
|
253
|
+
InvalidResponseBodyError,
|
|
254
|
+
NotFoundError,
|
|
255
|
+
RateLimitError,
|
|
256
|
+
ScrapelyApiError,
|
|
257
|
+
ScrapelyClientError,
|
|
258
|
+
ServerError,
|
|
259
|
+
UnauthorizedError,
|
|
260
|
+
raise_for_status,
|
|
261
|
+
)
|
|
196
262
|
from .events import (
|
|
197
263
|
AbortingEvent,
|
|
198
264
|
ActorEventTypes,
|
|
@@ -216,6 +282,19 @@ if TYPE_CHECKING: # Static analysers and IDEs see the lazy names as plain impor
|
|
|
216
282
|
SystemInfoEventData,
|
|
217
283
|
UnknownEvent,
|
|
218
284
|
)
|
|
285
|
+
from .pricing_types import (
|
|
286
|
+
ActorChargeEvent,
|
|
287
|
+
ActorPricingInfo,
|
|
288
|
+
ActorPricingInfoResult,
|
|
289
|
+
ChargeResult,
|
|
290
|
+
FlatPricePerMonthActorPricingInfo,
|
|
291
|
+
FreeActorPricingInfo,
|
|
292
|
+
PayPerEventActorPricingInfo,
|
|
293
|
+
PricePerDatasetItemActorPricingInfo,
|
|
294
|
+
PricingModel,
|
|
295
|
+
PricingPerEvent,
|
|
296
|
+
)
|
|
297
|
+
from .proxy_configuration import ProxyConfiguration
|
|
219
298
|
from .request_loaders import ScrapelyRequestList
|
|
220
299
|
from .storage import (
|
|
221
300
|
ApiStorageClient,
|
|
@@ -226,8 +305,27 @@ if TYPE_CHECKING: # Static analysers and IDEs see the lazy names as plain impor
|
|
|
226
305
|
StorageClient,
|
|
227
306
|
set_api_storage,
|
|
228
307
|
)
|
|
308
|
+
from .types import (
|
|
309
|
+
AbortOptions,
|
|
310
|
+
ActorInput,
|
|
311
|
+
ActorPermissionLevel,
|
|
312
|
+
ActorRun,
|
|
313
|
+
ApifyEnv,
|
|
314
|
+
CallOptions,
|
|
315
|
+
ExitOptions,
|
|
316
|
+
InitOptions,
|
|
317
|
+
MetamorphOptions,
|
|
318
|
+
OpenStorageOptions,
|
|
319
|
+
ProxyInfo,
|
|
320
|
+
RebootOptions,
|
|
321
|
+
RecordOptions,
|
|
322
|
+
SetStatusMessageOptions,
|
|
323
|
+
StartOptions,
|
|
324
|
+
WebhookOptions,
|
|
325
|
+
)
|
|
326
|
+
from .webhook import Webhook, WebhookEventType
|
|
229
327
|
|
|
230
|
-
__version__ = '0.1.
|
|
328
|
+
__version__ = '0.1.43'
|
|
231
329
|
__all__ = [
|
|
232
330
|
# Core
|
|
233
331
|
'Actor',
|
|
@@ -11,15 +11,29 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from scrapely_client import USES_PUSH_DATA_INTERCEPTION, ApifyClientAsync, push_data_charging_context
|
|
16
|
+
|
|
17
|
+
from .charging import ChargingManager
|
|
15
18
|
|
|
19
|
+
# ``scrapely_client`` (219 pydantic models) loads only when a client is created
|
|
20
|
+
# or one of its names below is used: ``Actor`` imports this module, and most
|
|
21
|
+
# runs never create an API client.
|
|
22
|
+
#
|
|
16
23
|
# Re-exported so the SDK and the client share one marker attribute and one
|
|
17
24
|
# context var. Identity matters: the client's intercepted ``push_items()``
|
|
18
25
|
# reads/writes these, and the SDK detects interception via the attribute.
|
|
19
|
-
|
|
26
|
+
_CLIENT_NAMES = frozenset({'ApifyClientAsync', 'USES_PUSH_DATA_INTERCEPTION', 'push_data_charging_context'})
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
|
|
29
|
+
def __getattr__(name: str) -> Any:
|
|
30
|
+
if name in _CLIENT_NAMES:
|
|
31
|
+
import scrapely_client
|
|
32
|
+
|
|
33
|
+
value = getattr(scrapely_client, name)
|
|
34
|
+
globals()[name] = value
|
|
35
|
+
return value
|
|
36
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
23
37
|
|
|
24
38
|
__all__ = [
|
|
25
39
|
'USES_PUSH_DATA_INTERCEPTION',
|
|
@@ -81,7 +95,7 @@ def create_charging_client(
|
|
|
81
95
|
charging_manager: Optional['ChargingManager'] = None,
|
|
82
96
|
default_dataset_id: Optional[str] = None,
|
|
83
97
|
**kwargs: Any,
|
|
84
|
-
) -> ApifyClientAsync:
|
|
98
|
+
) -> 'ApifyClientAsync':
|
|
85
99
|
"""Create an ``ApifyClientAsync`` configured with the SDK's charging hooks.
|
|
86
100
|
|
|
87
101
|
Args:
|
|
@@ -101,6 +115,8 @@ def create_charging_client(
|
|
|
101
115
|
if api_public_url:
|
|
102
116
|
kwargs['api_public_url'] = api_public_url
|
|
103
117
|
|
|
118
|
+
from scrapely_client import ApifyClientAsync
|
|
119
|
+
|
|
104
120
|
charging_hooks = _ChargingManagerHooks(charging_manager) if charging_manager is not None else None
|
|
105
121
|
|
|
106
122
|
return ApifyClientAsync(
|
|
@@ -30,11 +30,11 @@ Mirrors the Node SDK ``patch-browser-pool.ts``:
|
|
|
30
30
|
sandbox-safe Chromium args when running on the platform.
|
|
31
31
|
- Soft no-op when Playwright/Crawlee browsers are not installed (e.g. HTTP-only
|
|
32
32
|
actors).
|
|
33
|
-
- Crawlee is
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
moment
|
|
37
|
-
|
|
33
|
+
- Neither Crawlee, Playwright, Camoufox nor pyppeteer is imported by this
|
|
34
|
+
module (the base images import it at every interpreter start). Each patch is
|
|
35
|
+
applied at once when its module is already loaded, otherwise by a
|
|
36
|
+
post-import hook the moment that module finishes loading — before actor code
|
|
37
|
+
can use what it patches.
|
|
38
38
|
|
|
39
39
|
Location and proxy handling (the remote browser never takes a proxy):
|
|
40
40
|
|
|
@@ -67,19 +67,79 @@ Location and proxy handling (the remote browser never takes a proxy):
|
|
|
67
67
|
from __future__ import annotations
|
|
68
68
|
|
|
69
69
|
import contextvars
|
|
70
|
-
import functools
|
|
71
|
-
import importlib.abc
|
|
72
|
-
import logging
|
|
73
70
|
import os
|
|
74
|
-
import re
|
|
75
71
|
import sys
|
|
76
|
-
from typing import Any, List, Optional, Tuple
|
|
77
|
-
from urllib.parse import SplitResult, parse_qsl, unquote, urlencode, urlsplit, urlunsplit
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
# This module is imported at EVERY interpreter start in actor images (base
|
|
74
|
+
# image sitecustomize), before any actor code: it imports only builtin modules
|
|
75
|
+
# at load time. Everything else (typing, logging, re, urllib.parse, functools,
|
|
76
|
+
# asyncio via cdp_retry, the browser location table) loads on first use.
|
|
77
|
+
TYPE_CHECKING = False
|
|
78
|
+
if TYPE_CHECKING:
|
|
79
|
+
from typing import Any, List, Optional, Tuple
|
|
80
|
+
from urllib.parse import SplitResult
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
|
|
83
|
+
class _LazyLogger:
|
|
84
|
+
"""``logging.getLogger(__name__)``, imported on first use."""
|
|
85
|
+
|
|
86
|
+
def __getattr__(self, name: str) -> Any:
|
|
87
|
+
import logging
|
|
88
|
+
|
|
89
|
+
return getattr(logging.getLogger(__name__), name)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
logger = _LazyLogger()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def urlsplit(*args: Any, **kwargs: Any) -> Any:
|
|
96
|
+
from urllib.parse import urlsplit as _urlsplit
|
|
97
|
+
|
|
98
|
+
return _urlsplit(*args, **kwargs)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def urlunsplit(*args: Any, **kwargs: Any) -> Any:
|
|
102
|
+
from urllib.parse import urlunsplit as _urlunsplit
|
|
103
|
+
|
|
104
|
+
return _urlunsplit(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_qsl(*args: Any, **kwargs: Any) -> Any:
|
|
108
|
+
from urllib.parse import parse_qsl as _parse_qsl
|
|
109
|
+
|
|
110
|
+
return _parse_qsl(*args, **kwargs)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def urlencode(*args: Any, **kwargs: Any) -> Any:
|
|
114
|
+
from urllib.parse import urlencode as _urlencode
|
|
115
|
+
|
|
116
|
+
return _urlencode(*args, **kwargs)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def unquote(*args: Any, **kwargs: Any) -> Any:
|
|
120
|
+
from urllib.parse import unquote as _unquote
|
|
121
|
+
|
|
122
|
+
return _unquote(*args, **kwargs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ``cdp_retry`` pulls in asyncio and ``_browser_locations`` is a large table;
|
|
126
|
+
# both are only needed once a browser is actually launched.
|
|
127
|
+
def subdivision_to_state_name(country: Optional[str], subdivision: Optional[str]) -> Optional[str]:
|
|
128
|
+
from ._browser_locations import subdivision_to_state_name as _subdivision_to_state_name
|
|
129
|
+
|
|
130
|
+
return _subdivision_to_state_name(country, subdivision)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def wrap_connect_over_cdp(*args: Any, **kwargs: Any) -> Any:
|
|
134
|
+
from .cdp_retry import wrap_connect_over_cdp as _wrap_connect_over_cdp
|
|
135
|
+
|
|
136
|
+
return _wrap_connect_over_cdp(*args, **kwargs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def connect_with_retry_async(*args: Any, **kwargs: Any) -> Any:
|
|
140
|
+
from .cdp_retry import connect_with_retry_async as _connect_with_retry_async
|
|
141
|
+
|
|
142
|
+
return await _connect_with_retry_async(*args, **kwargs)
|
|
83
143
|
|
|
84
144
|
_cdp_url: Optional[str] = None
|
|
85
145
|
|
|
@@ -219,7 +279,7 @@ def _redact_proxy_url(proxy_url: str) -> str:
|
|
|
219
279
|
return f'{parts.scheme or "http"}://{userinfo}{parts.hostname}{port}'
|
|
220
280
|
|
|
221
281
|
|
|
222
|
-
|
|
282
|
+
_COUNTRY_FRAGMENT_PATTERN = r'^country-([A-Za-z]{2})(?:_([A-Za-z0-9]{1,3}))?$'
|
|
223
283
|
|
|
224
284
|
|
|
225
285
|
def _parse_proxy_url_location(proxy_url: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
|
|
@@ -229,8 +289,10 @@ def _parse_proxy_url_location(proxy_url: Optional[str]) -> Tuple[Optional[str],
|
|
|
229
289
|
parts = _parse_proxy_url(str(proxy_url))
|
|
230
290
|
if not parts or not parts.username:
|
|
231
291
|
return None, None
|
|
292
|
+
import re
|
|
293
|
+
|
|
232
294
|
for fragment in unquote(parts.username).split(','):
|
|
233
|
-
match =
|
|
295
|
+
match = re.match(_COUNTRY_FRAGMENT_PATTERN, fragment.strip())
|
|
234
296
|
if match:
|
|
235
297
|
subdivision = match.group(2)
|
|
236
298
|
return match.group(1).upper(), subdivision.upper() if subdivision else None
|
|
@@ -475,6 +537,8 @@ def _apply_crawler_patch() -> None:
|
|
|
475
537
|
if getattr(PlaywrightCrawler, '_scrapely_browser_config_patched', False):
|
|
476
538
|
return
|
|
477
539
|
|
|
540
|
+
import functools
|
|
541
|
+
|
|
478
542
|
_orig_init = PlaywrightCrawler.__init__
|
|
479
543
|
|
|
480
544
|
@functools.wraps(_orig_init)
|
|
@@ -784,39 +848,46 @@ class _AsyncPlaywrightContextManagerProxy:
|
|
|
784
848
|
return await self._cm.__aexit__(*exc)
|
|
785
849
|
|
|
786
850
|
|
|
787
|
-
def
|
|
788
|
-
"""Wrap playwright
|
|
851
|
+
def _apply_raw_playwright_async_patch() -> None:
|
|
852
|
+
"""Wrap ``playwright.async_api.async_playwright`` to redirect raw launches to CDP."""
|
|
789
853
|
try:
|
|
790
854
|
import playwright.async_api as playwright_async_api
|
|
791
855
|
except ImportError:
|
|
792
|
-
|
|
856
|
+
return
|
|
793
857
|
|
|
794
|
-
if playwright_async_api
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
858
|
+
if getattr(playwright_async_api, '_scrapely_raw_cdp_patched', False):
|
|
859
|
+
return
|
|
860
|
+
orig_async = playwright_async_api.async_playwright
|
|
861
|
+
|
|
862
|
+
def async_playwright_patched() -> Any:
|
|
863
|
+
return _AsyncPlaywrightContextManagerProxy(orig_async())
|
|
798
864
|
|
|
799
|
-
|
|
800
|
-
|
|
865
|
+
playwright_async_api.async_playwright = async_playwright_patched
|
|
866
|
+
playwright_async_api._scrapely_raw_cdp_patched = True # type: ignore[attr-defined]
|
|
801
867
|
|
|
802
|
-
playwright_async_api.async_playwright = async_playwright_patched
|
|
803
|
-
playwright_async_api._scrapely_raw_cdp_patched = True # type: ignore[attr-defined]
|
|
804
868
|
|
|
869
|
+
def _apply_raw_playwright_sync_patch() -> None:
|
|
870
|
+
"""Wrap ``playwright.sync_api.sync_playwright`` to redirect raw launches to CDP."""
|
|
805
871
|
try:
|
|
806
872
|
import playwright.sync_api as playwright_sync_api
|
|
807
873
|
except ImportError:
|
|
808
|
-
|
|
874
|
+
return
|
|
809
875
|
|
|
810
|
-
if playwright_sync_api
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
orig_sync = playwright_sync_api.sync_playwright
|
|
876
|
+
if getattr(playwright_sync_api, '_scrapely_raw_cdp_patched', False):
|
|
877
|
+
return
|
|
878
|
+
orig_sync = playwright_sync_api.sync_playwright
|
|
814
879
|
|
|
815
|
-
|
|
816
|
-
|
|
880
|
+
def sync_playwright_patched() -> Any:
|
|
881
|
+
return _SyncPlaywrightContextManagerProxy(orig_sync())
|
|
817
882
|
|
|
818
|
-
|
|
819
|
-
|
|
883
|
+
playwright_sync_api.sync_playwright = sync_playwright_patched
|
|
884
|
+
playwright_sync_api._scrapely_raw_cdp_patched = True # type: ignore[attr-defined]
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _apply_raw_playwright_patch() -> None:
|
|
888
|
+
"""Wrap playwright's sync/async entry points to redirect raw launches to CDP."""
|
|
889
|
+
_apply_raw_playwright_async_patch()
|
|
890
|
+
_apply_raw_playwright_sync_patch()
|
|
820
891
|
|
|
821
892
|
|
|
822
893
|
def _apply_pyppeteer_patch() -> None:
|
|
@@ -874,31 +945,59 @@ def _apply_pyppeteer_patch() -> None:
|
|
|
874
945
|
pyppeteer_mod._scrapely_cdp_patched = True
|
|
875
946
|
|
|
876
947
|
|
|
877
|
-
# ───
|
|
948
|
+
# ─── Patches applied when their library loads ────────────────────────────────
|
|
878
949
|
#
|
|
879
|
-
#
|
|
880
|
-
#
|
|
881
|
-
#
|
|
882
|
-
#
|
|
883
|
-
#
|
|
884
|
-
#
|
|
885
|
-
# by
|
|
950
|
+
# This module is imported at every interpreter start in actor images (base
|
|
951
|
+
# image sitecustomize), so it never imports Crawlee, Playwright, Camoufox or
|
|
952
|
+
# pyppeteer itself: importing them up front cost ~1 s per process on the
|
|
953
|
+
# platform even when the actor never opens a browser. Every patch mutates a
|
|
954
|
+
# module or class in place, so it only has to run before actor code can use
|
|
955
|
+
# what it patches: it is applied at once when the defining module is already
|
|
956
|
+
# loaded, otherwise by a post-import hook the moment that module finishes
|
|
957
|
+
# executing, before the import statement that loaded it returns
|
|
958
|
+
# (``import crawlee.browsers``, ``from crawlee.crawlers import
|
|
959
|
+
# PlaywrightCrawler``, ``from playwright.async_api import async_playwright``, a
|
|
960
|
+
# PlaywrightCrawler built by any template...). Behaviour is the same as
|
|
961
|
+
# applying them eagerly.
|
|
886
962
|
|
|
887
963
|
# Module name -> patch functions to run once that module has finished loading.
|
|
888
964
|
# ``crawlee.browsers`` imports BrowserPool, PlaywrightBrowserController and
|
|
889
965
|
# PlaywrightBrowserPlugin in its ``__init__``; any import of one of those
|
|
890
|
-
# submodules runs that ``__init__`` to completion first.
|
|
891
|
-
|
|
966
|
+
# submodules runs that ``__init__`` to completion first. Likewise importing
|
|
967
|
+
# ``pyppeteer.launcher`` runs the ``pyppeteer`` package first, and the
|
|
968
|
+
# ``camoufox`` package defines the ``AsyncNewBrowser`` the patch replaces.
|
|
969
|
+
_POST_IMPORT_PATCHES: dict = {
|
|
892
970
|
'crawlee.browsers': (_apply_patch,),
|
|
893
971
|
'crawlee.crawlers._playwright._playwright_crawler': (_apply_crawler_patch,),
|
|
972
|
+
'playwright.async_api': (_apply_raw_playwright_async_patch,),
|
|
973
|
+
'playwright.sync_api': (_apply_raw_playwright_sync_patch,),
|
|
974
|
+
'camoufox': (_apply_camoufox_patch,),
|
|
975
|
+
'pyppeteer': (_apply_pyppeteer_patch,),
|
|
894
976
|
}
|
|
977
|
+
# Kept under its old name for code that inspects it.
|
|
978
|
+
_CRAWLEE_POST_IMPORT_PATCHES = _POST_IMPORT_PATCHES
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _run_patches(module_name: str, patches: tuple) -> None:
|
|
982
|
+
"""Run the patches for a loaded module; a failing patch never breaks the actor's import."""
|
|
983
|
+
for patch in patches:
|
|
984
|
+
try:
|
|
985
|
+
patch()
|
|
986
|
+
except Exception as exc: # pragma: no cover - defensive (incompatible library version)
|
|
987
|
+
logger.warning(
|
|
988
|
+
'[scrapely] Could not patch %s for the remote browser (%s: %s)',
|
|
989
|
+
module_name,
|
|
990
|
+
type(exc).__name__,
|
|
991
|
+
exc,
|
|
992
|
+
)
|
|
895
993
|
|
|
896
994
|
|
|
897
|
-
class _PostImportLoader
|
|
995
|
+
class _PostImportLoader:
|
|
898
996
|
"""Delegating loader that runs the Scrapely patches after the module executes."""
|
|
899
997
|
|
|
900
|
-
def __init__(self, loader: Any, patches: tuple) -> None:
|
|
998
|
+
def __init__(self, loader: Any, fullname: str, patches: tuple) -> None:
|
|
901
999
|
self._loader = loader
|
|
1000
|
+
self._fullname = fullname
|
|
902
1001
|
self._patches = patches
|
|
903
1002
|
|
|
904
1003
|
def create_module(self, spec: Any) -> Any:
|
|
@@ -914,22 +1013,25 @@ class _PostImportLoader(importlib.abc.Loader):
|
|
|
914
1013
|
except Exception:
|
|
915
1014
|
pass
|
|
916
1015
|
self._loader.exec_module(module)
|
|
917
|
-
|
|
918
|
-
|
|
1016
|
+
# The module loaded: patch it now, once. A failed load (the exception
|
|
1017
|
+
# above) keeps the hook, so a later successful import is patched too.
|
|
1018
|
+
if _POST_IMPORT_PATCHES.get(self._fullname) is self._patches:
|
|
1019
|
+
_POST_IMPORT_PATCHES.pop(self._fullname, None)
|
|
1020
|
+
_run_patches(self._fullname, self._patches)
|
|
919
1021
|
|
|
920
1022
|
def __getattr__(self, name: str) -> Any:
|
|
921
1023
|
return getattr(self._loader, name)
|
|
922
1024
|
|
|
923
1025
|
|
|
924
|
-
class
|
|
925
|
-
"""Meta path finder that hooks the
|
|
1026
|
+
class _PostImportFinder:
|
|
1027
|
+
"""Meta path finder that hooks the modules in ``_POST_IMPORT_PATCHES``."""
|
|
926
1028
|
|
|
927
1029
|
def find_spec(self, fullname: str, path: Any = None, target: Any = None) -> Any:
|
|
928
|
-
patches =
|
|
1030
|
+
patches = _POST_IMPORT_PATCHES.get(fullname)
|
|
929
1031
|
if patches is None:
|
|
930
1032
|
return None
|
|
931
1033
|
for finder in sys.meta_path:
|
|
932
|
-
if finder is self or isinstance(finder,
|
|
1034
|
+
if finder is self or isinstance(finder, _PostImportFinder):
|
|
933
1035
|
continue
|
|
934
1036
|
find_spec = getattr(finder, 'find_spec', None)
|
|
935
1037
|
if find_spec is None:
|
|
@@ -941,25 +1043,25 @@ class _CrawleePostImportFinder(importlib.abc.MetaPathFinder):
|
|
|
941
1043
|
return None
|
|
942
1044
|
if spec.loader is None or not hasattr(spec.loader, 'exec_module'):
|
|
943
1045
|
return spec
|
|
944
|
-
#
|
|
945
|
-
|
|
946
|
-
spec.loader = _PostImportLoader(spec.loader, patches)
|
|
1046
|
+
# The hook stays registered until the module has really loaded
|
|
1047
|
+
# (``importlib.util.find_spec`` availability checks do not consume it).
|
|
1048
|
+
spec.loader = _PostImportLoader(spec.loader, fullname, patches)
|
|
947
1049
|
return spec
|
|
948
1050
|
|
|
949
1051
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1052
|
+
# Kept under its old name for code that inspects ``sys.meta_path``.
|
|
1053
|
+
_CrawleePostImportFinder = _PostImportFinder
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _install_post_import_patches() -> None:
|
|
1057
|
+
"""Apply each patch now if its module is loaded, else when it loads."""
|
|
1058
|
+
for module_name in list(_POST_IMPORT_PATCHES):
|
|
953
1059
|
if module_name in sys.modules:
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
_install_crawlee_patches()
|
|
963
|
-
_apply_camoufox_patch()
|
|
964
|
-
_apply_raw_playwright_patch()
|
|
965
|
-
_apply_pyppeteer_patch()
|
|
1060
|
+
_run_patches(module_name, _POST_IMPORT_PATCHES.pop(module_name))
|
|
1061
|
+
if _POST_IMPORT_PATCHES and not any(isinstance(finder, _PostImportFinder) for finder in sys.meta_path):
|
|
1062
|
+
sys.meta_path.insert(0, _PostImportFinder())
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
_install_crawlee_patches = _install_post_import_patches
|
|
1066
|
+
|
|
1067
|
+
_install_post_import_patches()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/extensions/__init__.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/extensions/_graceful_stop.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/extensions/_httpcache.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/middlewares/__init__.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/scrapy/middlewares/apify_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/_file_system/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/scrapely_dataset_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/scrapely_kvs_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/scrapely_rq_client.py
RENAMED
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely/storage/scrapely_storage_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.42 → scrapely_python-0.1.43}/src/scrapely_python.egg-info/top_level.txt
RENAMED
|
File without changes
|