scrapely-python 0.1.14__tar.gz → 0.1.16__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.14/src/scrapely_python.egg-info → scrapely_python-0.1.16}/PKG-INFO +1 -1
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/pyproject.toml +1 -1
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/__init__.py +1 -1
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/actor.py +41 -1
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/charging.py +13 -3
- scrapely_python-0.1.16/src/scrapely/solver.py +875 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/api_client.py +125 -26
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/client.py +5 -1
- {scrapely_python-0.1.14 → scrapely_python-0.1.16/src/scrapely_python.egg-info}/PKG-INFO +1 -1
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely_python.egg-info/SOURCES.txt +3 -0
- scrapely_python-0.1.16/tests/test_push_idempotency.py +242 -0
- scrapely_python-0.1.16/tests/test_solver.py +327 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_terminal_status_message.py +19 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/LICENSE +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/NOTICE +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/README.md +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/setup.cfg +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/cdp_retry.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/charging_client.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/patch_browser_pool.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/proxy_configuration.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely_python.egg-info/requires.txt +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/src/scrapely_python.egg-info/top_level.txt +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_api_client_retries.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_apify_parity.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_browser_configuration.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_cdp_retry.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_charge_limit_guard.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_charge_retries.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_charging_parity.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_charging_pending.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_input_proxy_aliases.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_parity.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_patch_browser_pool.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_proxy_configuration.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_run_control.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_secrets.py +0 -0
- {scrapely_python-0.1.14 → scrapely_python-0.1.16}/tests/test_storage.py +0 -0
|
@@ -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.16"
|
|
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"
|
|
@@ -1054,7 +1054,19 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1054
1054
|
else:
|
|
1055
1055
|
logger.info(logged)
|
|
1056
1056
|
|
|
1057
|
-
|
|
1057
|
+
if is_terminal:
|
|
1058
|
+
result = await cls._put_run_status_message(message, is_terminal=True)
|
|
1059
|
+
else:
|
|
1060
|
+
# A progress update is best effort: if the API still fails after the
|
|
1061
|
+
# retry window (e.g. a long 503), log it and carry on instead of
|
|
1062
|
+
# crashing the run. Terminal updates keep raising.
|
|
1063
|
+
try:
|
|
1064
|
+
result = await cls._put_run_status_message(message, is_terminal=False)
|
|
1065
|
+
except asyncio.CancelledError:
|
|
1066
|
+
raise
|
|
1067
|
+
except Exception as e: # noqa: BLE001
|
|
1068
|
+
logger.warning('Failed to update the status message (continuing): %s', e)
|
|
1069
|
+
return ActorRun(id='', actor_id='', status='')
|
|
1058
1070
|
if result is None:
|
|
1059
1071
|
return ActorRun(id='', actor_id='', status='')
|
|
1060
1072
|
run_id, data = result
|
|
@@ -1068,6 +1080,34 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1068
1080
|
status_message=data.get('statusMessage'),
|
|
1069
1081
|
)
|
|
1070
1082
|
|
|
1083
|
+
@classmethod
|
|
1084
|
+
async def solver_status(cls, vendor: str) -> Dict[str, Any]:
|
|
1085
|
+
"""Anti-bot solver status for a vendor (free, cached for 15 s, never raises).
|
|
1086
|
+
|
|
1087
|
+
``usable`` False means the vendor cannot be solved right now: take your
|
|
1088
|
+
fallback (for example the hosted browser), or fail the items that need
|
|
1089
|
+
a solve at once and call ``set_solver_degraded_status()``. See
|
|
1090
|
+
``scrapely.solver``.
|
|
1091
|
+
"""
|
|
1092
|
+
from .solver import solver_status_async
|
|
1093
|
+
|
|
1094
|
+
return await solver_status_async(vendor)
|
|
1095
|
+
|
|
1096
|
+
@classmethod
|
|
1097
|
+
async def set_solver_degraded_status(cls, skipped: Optional[int] = None, total: Optional[int] = None) -> None:
|
|
1098
|
+
"""Set the standard status message for a run whose site protection cannot be solved right now.
|
|
1099
|
+
|
|
1100
|
+
"Site protection temporarily unavailable, results may be incomplete",
|
|
1101
|
+
with "(N of M items skipped)" when both counts are given. Never raises:
|
|
1102
|
+
the run goes on.
|
|
1103
|
+
"""
|
|
1104
|
+
from .solver import degraded_status_message
|
|
1105
|
+
|
|
1106
|
+
try:
|
|
1107
|
+
await cls.set_status_message(degraded_status_message(skipped, total))
|
|
1108
|
+
except Exception as exc: # noqa: BLE001 - a status message must never stop the run
|
|
1109
|
+
logger.warning(f'Could not set the solver status message: {exc}')
|
|
1110
|
+
|
|
1071
1111
|
@classmethod
|
|
1072
1112
|
async def _put_run_status_message(
|
|
1073
1113
|
cls, message: str, *, is_terminal: bool
|
|
@@ -52,7 +52,7 @@ _PENDING_MAX_ENTRIES = 1024
|
|
|
52
52
|
# Same-key POST attempts before leaving an intent pending / re-queueing a void.
|
|
53
53
|
# The API client itself retries 429 and 5xx answers and refused or reset
|
|
54
54
|
# connections of these POSTs with exponential backoff (honouring Retry-After)
|
|
55
|
-
# for its retry window (
|
|
55
|
+
# for its retry window (5 minutes, SCRAPELY_API_RETRY_WINDOW_SECS): re-sending
|
|
56
56
|
# is safe because every attempt carries the same Idempotency-Key. These outer
|
|
57
57
|
# attempts cover what the client gives up on quickly (timeouts, other
|
|
58
58
|
# transport errors), within _RETRY_BUDGET_SECS. Then the charge stays pending
|
|
@@ -62,7 +62,7 @@ _POST_ATTEMPTS = 8
|
|
|
62
62
|
|
|
63
63
|
# Outer retries stop once this much time has passed since the first attempt
|
|
64
64
|
# (the API retry window; JS SDK parity).
|
|
65
|
-
_RETRY_BUDGET_SECS =
|
|
65
|
+
_RETRY_BUDGET_SECS = 300.0
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
def _is_transient_status(status: Optional[int]) -> bool:
|
|
@@ -1567,10 +1567,20 @@ def _push_certainly_failed(err: BaseException) -> bool:
|
|
|
1567
1567
|
"""True when a dataset push certainly stored nothing.
|
|
1568
1568
|
|
|
1569
1569
|
The storage API answered with an error status, or the connection was never
|
|
1570
|
-
made. Timeouts and resets after sending are not proof: the write may have landed
|
|
1570
|
+
made. Timeouts and resets after sending are not proof: the write may have landed,
|
|
1571
|
+
and neither is any error of a push one of whose earlier attempts may have been
|
|
1572
|
+
applied (``outcome_unknown``, set by the storage client's retries).
|
|
1571
1573
|
"""
|
|
1572
1574
|
import httpx
|
|
1573
1575
|
|
|
1576
|
+
c: Optional[BaseException] = err
|
|
1577
|
+
for _ in range(4):
|
|
1578
|
+
if c is None:
|
|
1579
|
+
break
|
|
1580
|
+
if getattr(c, 'outcome_unknown', False) is True:
|
|
1581
|
+
return False
|
|
1582
|
+
c = c.__cause__ or c.__context__
|
|
1583
|
+
|
|
1574
1584
|
e: Optional[BaseException] = err
|
|
1575
1585
|
for _ in range(4):
|
|
1576
1586
|
if e is None:
|